first commit

This commit is contained in:
CHIEFSOFT\ameye
2024-09-30 18:11:26 -04:00
commit e592ca6823
27270 changed files with 5002257 additions and 0 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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 <stevani@hotmail.com.au>
* @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
File diff suppressed because one or more lines are too long
@@ -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 <stevani@hotmail.com.au>
* @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.getString)("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
@@ -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 <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Media Manager commands.\n *\n * @module tiny_accessibilitychecker/commands\n * @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {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,kBAAU,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"}
@@ -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
@@ -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 <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Media common values.\n *\n * @module tiny_accessibilitychecker/common\n * @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\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"}
@@ -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
@@ -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 <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Media Manager configuration.\n *\n * @module tiny_accessibilitychecker/configuration\n * @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {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"}
@@ -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 <stevani@hotmail.com.au>
* @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
@@ -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 <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Media Manager plugin for Moodle.\n *\n * @module tiny_accessibilitychecker/plugin\n * @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>\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\n// eslint-disable-next-line no-async-promise-executor\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":";;;;;;;kMA8Be,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"}
@@ -0,0 +1,501 @@
// 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 <http://www.gnu.org/licenses/>.
/*
* @package tiny_accessibilitychecker
* @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Templates from 'core/templates';
import {getString, getStrings} from 'core/str';
import {component} from './common';
import Modal from 'core/modal';
import * as ModalEvents from 'core/modal_events';
import ColorBase from './colorbase';
import {getPlaceholderSelectors} from 'editor_tiny/options';
/**
* @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;
this.placeholderSelectors = null;
const placeholders = getPlaceholderSelectors(this.editor);
if (placeholders.length) {
this.placeholderSelectors = placeholders.join(', ');
}
}
destroy() {
delete this.editor;
delete this.colorBase;
this.modal.destroy();
delete this.modal;
}
async displayDialogue() {
this.modal = await Modal.create({
large: true,
title: getString('pluginname', component),
body: this.getDialogueContent(),
show: true,
});
// 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();
});
}
async getAllWarningStrings() {
const keys = [
'emptytext',
'entiredocument',
'imagesmissingalt',
'needsmorecontrast',
'needsmoreheadings',
'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<Array>} 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].filter((node) => {
// If the failed node is a placeholder element. We should remove it from the list.
if (node !== this.editor && this.placeholderSelectors) {
return node.matches(this.placeholderSelectors) === false;
}
return node;
}).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.hasChildNodes() && node.childNodes[0].nodeValue !== null)
.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) {
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() === 'rgba(0, 0, 0, 0)' || 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
];
}
}
@@ -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 <http://www.gnu.org/licenses/>.
/*
* @package tiny_accessibilitychecker
* @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>
* @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;
};
}
@@ -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 <http://www.gnu.org/licenses/>.
/**
* Tiny Media Manager commands.
*
* @module tiny_accessibilitychecker/commands
* @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {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();
}
});
};
};
@@ -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 <http://www.gnu.org/licenses/>.
/**
* Tiny Media common values.
*
* @module tiny_accessibilitychecker/common
* @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>
* @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',
};
@@ -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 <http://www.gnu.org/licenses/>.
/**
* Tiny Media Manager configuration.
*
* @module tiny_accessibilitychecker/configuration
* @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>
* @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)
};
};
@@ -0,0 +1,51 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Tiny Media Manager plugin for Moodle.
*
* @module tiny_accessibilitychecker/plugin
* @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>
* @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';
// eslint-disable-next-line no-async-promise-executor
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]);
});
@@ -0,0 +1,61 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
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 <stevani@hotmail.com.au>
* @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,
];
}
}
@@ -0,0 +1,35 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace tiny_accessibilitychecker\privacy;
/**
* Privacy Subsystem implementation for the accessibilitychecker plugin for TinyMCE.
*
* @package tiny_accessibilitychecker
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @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';
}
}
@@ -0,0 +1,38 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* Strings for component 'tiny_accessibilitychecker', language 'en'.
*
* @package tiny_accessibilitychecker
* @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>
* @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 allow screen reader users to navigate through the page easily and make the page more usable for everyone.';
$string['nowarnings'] = 'Congratulations, no accessibility issues found!';
$string['pluginname'] = 'Accessibility checker';
$string['report'] = 'Accessibility report:';
$string['tablesmissingcaption'] = 'A table caption is not required, but is generally helpful.';
$string['tablesmissingheaders'] = 'Tables should use row and/or column headers.';
$string['tableswithmergedcells'] = 'Tables should not contain merged cells, as screen readers may not support them.';
$string['privacy:metadata'] = 'The accessibility checker for TinyMCE does not store any personal data.';
$string['viewissue'] = 'View';
$string['helplinktext'] = 'Accessibility helper';
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1"
id="svg2" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" sodipodi:docname="universal-access.svg" inkscape:version="0.48.4 r9939"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="1200px" height="1200px"
viewBox="0 0 1200 1200" enable-background="new 0 0 1200 1200" xml:space="preserve" preserveAspectRatio="xMinYMid meet">
<path id="path24717" inkscape:connector-curvature="0" d="M600,0C268.629,0,0,268.629,0,600s268.629,600,600,600
s600-268.629,600-600S931.371,0,600,0z M600,80.167c287.105,0,519.833,232.727,519.833,519.833
c0,287.105-232.727,519.871-519.833,519.871c-287.106,0-519.871-232.765-519.871-519.871S312.894,80.167,600,80.167z
M922.375,541.163c17.269,16.33,16.732,39.747,3.838,56.29c-15.771,16.638-40.629,17.347-56.29,3.838
c-48.914-44.386-102.442-85.918-152.237-131.77c-0.152,1.96-13.502-8.813-19.189-8.956c-5.118,0-7.677,5.971-7.677,17.91
c2.512,49.275-1.148,101.299,4.478,149.04c2.133,17.484,3.198,27.506,3.198,30.064l57.569,327.505
c3.299,30.075-13.141,52.539-39.658,57.569c-26.557,7.224-53.846-15.676-57.569-39.658c0,0-46.445-264.445-47.335-268.657
s-2.46-27.537-11.514-28.784c-11.634,4.222-10.286,23.812-11.516,28.784c-1.229,4.972-47.335,268.657-47.335,268.657
c-7.78,27.743-31.696,44.14-57.568,39.658c-29.562-7.392-44.012-31.018-39.658-57.569l57.569-328.784
c8.5-60.769,6.396-115.129,6.396-176.546c0-11.944-2.345-18.128-7.036-18.55c-4.69-0.427-10.874,2.771-18.55,9.595L330.051,601.29
c-17.788,13.255-42.655,11.046-56.29-3.838c-13.784-19.696-12.902-41.007,3.838-56.29l199.573-176.546
c9.384-5.972,18.126-10.021,26.228-12.154c8.104-2.132,18.125-3.198,30.062-3.198h133.05c11.939,0,21.962,1.066,30.063,3.198
c8.103,2.133,16.845,6.61,26.227,13.433C786.344,421.632,853.806,481.598,922.375,541.163L922.375,541.163z M688.031,238.326
c0,48.328-39.178,87.505-87.504,87.505c-48.328,0-87.506-39.177-87.506-87.505c0-48.327,39.178-87.504,87.506-87.504
C648.854,150.821,688.031,189.999,688.031,238.326z"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

@@ -0,0 +1,7 @@
.warning-desc {
word-wrap: break-word;
}
.warning-desc li {
margin-bottom: 10px;
}
@@ -0,0 +1,57 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template tiny_accessibilitychecker/warning_content
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
}]
}
]
}
}}
<div class="warning-desc">
{{#warnings}}
<p>{{{description}}}</p>
<ol class="accessibilitywarnings">
{{#nodeData}}
<li>
{{#text}}
{{text}}
{{/text}}
{{#src}}
<img alt="" src="{{src}}" width="100"/>
{{/src}}
- <a href="#" data-action="highlightfault" data-node-name="{{nodeName}}" data-node-index="{{nodeIndex}}">{{#str}} viewissue, tiny_accessibilitychecker {{/str}}</a>
</li>
{{/nodeData}}
</ol>
{{/warnings}}
{{^warnings}}
<p>{{#str}} nowarnings, tiny_accessibilitychecker {{/str}}</p>
{{/warnings}}
</div>
@@ -0,0 +1,54 @@
@editor @editor_tiny @tiny_accessibilitychecker
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 "<content>"
When I click on the "Tools > Accessibility checker" menu item for the "Description" TinyMCE editor
Then I should see "<result>" in the "Accessibility checker" "dialogue"
Examples:
| result | content |
| The colours of the foreground and background text do not have enough contrast. | <p style='color: #7c7cff; background-color: #ffffff;'>Hard to read</p> |
| There is a lot of text with no headings. | <p>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.</p><p>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.</p> |
| Tables should not contain merged cells, as screen readers may not support them.| <table><caption>Dogs that look good in pants</caption><tr><th>Breed</th><th>Coolness</th></tr><tr><td>Poodle</td><td rowspan='2'>NOT COOL</td></tr><tr><td>Doberman</td></tr></table> |
| Tables should use row and/or column headers. | <table><caption>Dogs that look good in pants</caption><tr><th>Breed</th><td>Coolness</td></tr><tr><td>Poodle</td><td>NOT COOL</td></tr><tr><td>Doberman</td><td>COOL</td></tr></table> |
| A table caption is not required, but is generally helpful. | <table><tr><th>Breed</th><th>Coolness</th></tr><tr><td>Poodle</td><td>NOT COOL</td></tr><tr><td>Doberman</td><td>COOL</td></tr></table> |
@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 "<p>Some plain text</p><img src='http://download.moodle.org/unittest/test.jpg' width='1' height='1'/><p>Some more text</p>"
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 I wait "1" seconds
And I click on "This image is decorative only" "checkbox"
And I set the field "How would you describe this image to someone who can't see it?" to "No more warning!"
And I press "Save"
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 "URL" to "http://download.moodle.org/unittest/test.jpg"
And I click on "Add" "button" in the "Insert image" "dialogue"
And I wait "1" seconds
And I set the field "How would you describe this image to someone who can't see it?" to ""
And I click on "This image is decorative only" "checkbox"
When I press "Save"
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"
@javascript
Scenario: Placeholder element will not be assessed by accessibility checker
Given I log in as "admin"
And I open my profile in edit mode
When I set the field "Description" to "<p>Some plain text</p><img src='/broken-image' width='1' height='1' class='behat-tinymce-placeholder'/><p>Some more text</p>"
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"
@@ -0,0 +1,7 @@
This files describes API changes in tiny_accessibilitychecker - TinyMCE Accessibility checker plugin,
information provided here is intended especially for developers.
=== 4.2 ===
* The placeholder elements which were registered in placeholderSelectors in editor_tiny/options will not be
assessed by the accessibility checker plugin.
@@ -0,0 +1,29 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* Tiny media plugin version details.
*
* @package tiny_accessibilitychecker
* @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->component = 'tiny_accessibilitychecker';
@@ -0,0 +1,10 @@
define("tiny_autosave/autosaver",["exports","./options","./storage","core/log","core_form/events","./common"],(function(_exports,Options,Storage,_log,_events,_common){var obj;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}
/**
* Storage helper for the Moodle Tiny Autosave plugin.
*
* @module tiny_autosave/autosaver
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.register=void 0,Options=_interopRequireWildcard(Options),Storage=_interopRequireWildcard(Storage),_log=(obj=_log)&&obj.__esModule?obj:{default:obj};_exports.register=editor=>{const undoHandler=()=>{editor.undoManager.hasUndo()?Storage.saveDraft(editor):_log.default.debug("Ignoring undo event as there is no undo history",(0,_common.getLogSource)(editor))},visibilityChangedHandler=()=>{"hidden"===document.visibilityState&&Options.isInitialised(editor)&&Storage.saveDraft(editor)},handleFormSubmittedByJavascript=e=>{Options.isInitialised(editor)&&e.target.contains(editor.getElement())&&removeAutoSaveSession()},removeAutoSaveSession=()=>{document.removeEventListener("visibilitychange",visibilityChangedHandler),document.removeEventListener(_events.eventTypes.formSubmittedByJavascript,handleFormSubmittedByJavascript),Storage.removeAutosaveSession(editor)};document.addEventListener("visibilitychange",visibilityChangedHandler),editor.on("submit",removeAutoSaveSession),document.addEventListener(_events.eventTypes.formSubmittedByJavascript,handleFormSubmittedByJavascript),editor.on("init",(()=>{editor.on("AddUndo",undoHandler),editor.dom.isEmpty(editor.getBody())?(_log.default.info("Attempting to restore draft",(0,_common.getLogSource)(editor)),Storage.restoreDraft(editor)):(_log.default.warn("Skipping draft restoration. The editor is not empty.",(0,_common.getLogSource)(editor)),Options.markInitialised(editor))}))}}));
//# sourceMappingURL=autosaver.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"autosaver.min.js","sources":["../src/autosaver.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Storage helper for the Moodle Tiny Autosave plugin.\n *\n * @module tiny_autosave/autosaver\n * @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as Options from './options';\nimport * as Storage from './storage';\nimport Log from 'core/log';\nimport {eventTypes} from 'core_form/events';\nimport {getLogSource} from './common';\n\nexport const register = (editor) => {\n const undoHandler = () => {\n if (!editor.undoManager.hasUndo()) {\n Log.debug(`Ignoring undo event as there is no undo history`, getLogSource(editor));\n return;\n }\n Storage.saveDraft(editor);\n };\n\n const visibilityChangedHandler = () => {\n if (document.visibilityState === 'hidden') {\n if (Options.isInitialised(editor)) {\n Storage.saveDraft(editor);\n }\n }\n };\n\n // Javascript form submission handler.\n const handleFormSubmittedByJavascript = (e) => {\n if (Options.isInitialised(editor) && e.target.contains(editor.getElement())) {\n removeAutoSaveSession();\n }\n };\n\n // Remove the auto save session.\n const removeAutoSaveSession = () => {\n document.removeEventListener('visibilitychange', visibilityChangedHandler);\n document.removeEventListener(eventTypes.formSubmittedByJavascript, handleFormSubmittedByJavascript);\n Storage.removeAutosaveSession(editor);\n };\n\n // Attempt to store the draft one final time before the page unloads.\n // Note: This may need to be sent as a beacon instead.\n document.addEventListener('visibilitychange', visibilityChangedHandler);\n\n // When the page is submitted as a form, remove the draft.\n editor.on('submit', removeAutoSaveSession);\n document.addEventListener(eventTypes.formSubmittedByJavascript, handleFormSubmittedByJavascript);\n\n editor.on('init', () => {\n // Setup the Undo handler.\n editor.on('AddUndo', undoHandler);\n\n if (editor.dom.isEmpty(editor.getBody())) {\n Log.info(`Attempting to restore draft`, getLogSource(editor));\n Storage.restoreDraft(editor);\n } else {\n // There was nothing to restore, so we can mark the editor as initialised.\n Log.warn(`Skipping draft restoration. The editor is not empty.`, getLogSource(editor));\n Options.markInitialised(editor);\n }\n });\n};\n"],"names":["editor","undoHandler","undoManager","hasUndo","Storage","saveDraft","debug","visibilityChangedHandler","document","visibilityState","Options","isInitialised","handleFormSubmittedByJavascript","e","target","contains","getElement","removeAutoSaveSession","removeEventListener","eventTypes","formSubmittedByJavascript","removeAutosaveSession","addEventListener","on","dom","isEmpty","getBody","info","restoreDraft","warn","markInitialised"],"mappings":";;;;;;;4OA6ByBA,eACfC,YAAc,KACXD,OAAOE,YAAYC,UAIxBC,QAAQC,UAAUL,qBAHVM,yDAAyD,wBAAaN,UAM5EO,yBAA2B,KACI,WAA7BC,SAASC,iBACLC,QAAQC,cAAcX,SACtBI,QAAQC,UAAUL,SAMxBY,gCAAmCC,IACjCH,QAAQC,cAAcX,SAAWa,EAAEC,OAAOC,SAASf,OAAOgB,eAC1DC,yBAKFA,sBAAwB,KAC1BT,SAASU,oBAAoB,mBAAoBX,0BACjDC,SAASU,oBAAoBC,mBAAWC,0BAA2BR,iCACnER,QAAQiB,sBAAsBrB,SAKlCQ,SAASc,iBAAiB,mBAAoBf,0BAG9CP,OAAOuB,GAAG,SAAUN,uBACpBT,SAASc,iBAAiBH,mBAAWC,0BAA2BR,iCAEhEZ,OAAOuB,GAAG,QAAQ,KAEdvB,OAAOuB,GAAG,UAAWtB,aAEjBD,OAAOwB,IAAIC,QAAQzB,OAAO0B,yBACtBC,oCAAoC,wBAAa3B,SACrDI,QAAQwB,aAAa5B,uBAGjB6B,6DAA6D,wBAAa7B,SAC9EU,QAAQoB,gBAAgB9B"}
@@ -0,0 +1,3 @@
define("tiny_autosave/common",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={component:"tiny_autosave",pluginName:"tiny_autosave/plugin",getLogSource:editor=>"tiny_autosave/".concat(editor.id)},_exports.default}));
//# sourceMappingURL=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 <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Autosave plugin for Moodle.\n *\n * @module tiny_autosave/common\n * @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n component: 'tiny_autosave',\n pluginName: 'tiny_autosave/plugin',\n getLogSource: (editor) => `tiny_autosave/${editor.id}`,\n};\n"],"names":["component","pluginName","getLogSource","editor","id"],"mappings":"sKAuBe,CACXA,UAAW,gBACXC,WAAY,uBACZC,aAAeC,gCAA4BA,OAAOC"}
@@ -0,0 +1,11 @@
define("tiny_autosave/options",["exports","./common","editor_tiny/options","editor_tiny/utils"],(function(_exports,_common,_options,_utils){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getBackoffTime=void 0,Object.defineProperty(_exports,"getContextId",{enumerable:!0,get:function(){return _options.getContextId}}),Object.defineProperty(_exports,"getDraftItemId",{enumerable:!0,get:function(){return _options.getDraftItemId}}),_exports.setAutosaveHasReset=_exports.register=_exports.markInitialised=_exports.isInitialised=_exports.hasAutosaveHasReset=_exports.getPageInstance=_exports.getPageHash=void 0;
/**
* Options helper for the Moodle Tiny Autosave plugin.
*
* @module tiny_autosave/options
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const initialisedOptionName=(0,_options.getPluginOptionName)(_common.pluginName,"initialised"),pageHashName=(0,_options.getPluginOptionName)(_common.pluginName,"pagehash"),pageInstanceName=(0,_options.getPluginOptionName)(_common.pluginName,"pageinstance"),backoffTime=(0,_options.getPluginOptionName)(_common.pluginName,"backoffTime"),autosaveHasReset=(0,_options.getPluginOptionName)(_common.pluginName,"autosaveHasReset");_exports.register=editor=>{const registerOption=editor.options.register;registerOption(initialisedOptionName,{processor:"boolean",default:!1}),registerOption(pageHashName,{processor:"string",default:""}),registerOption(pageInstanceName,{processor:"string",default:""}),registerOption(pageInstanceName,{processor:"string",default:""}),registerOption(backoffTime,{processor:"number",default:500}),registerOption(autosaveHasReset,{processor:"boolean",default:!1})};_exports.isInitialised=editor=>!!(0,_utils.ensureEditorIsValid)(editor)&&editor.options.get(initialisedOptionName);_exports.markInitialised=editor=>editor.options.set(initialisedOptionName,!0);_exports.getPageHash=editor=>editor.options.get(pageHashName);_exports.getPageInstance=editor=>editor.options.get(pageInstanceName);_exports.getBackoffTime=editor=>editor.options.get(backoffTime);_exports.setAutosaveHasReset=editor=>editor.options.set(autosaveHasReset,!0);_exports.hasAutosaveHasReset=editor=>editor.options.get(autosaveHasReset)}));
//# sourceMappingURL=options.min.js.map
@@ -0,0 +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 <http://www.gnu.org/licenses/>.\n\n/**\n * Options helper for the Moodle Tiny Autosave plugin.\n *\n * @module tiny_autosave/options\n * @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {pluginName} from './common';\nimport {\n getContextId,\n getDraftItemId,\n getPluginOptionName,\n} from 'editor_tiny/options';\nimport {ensureEditorIsValid} from 'editor_tiny/utils';\n\nconst initialisedOptionName = getPluginOptionName(pluginName, 'initialised');\nconst pageHashName = getPluginOptionName(pluginName, 'pagehash');\nconst pageInstanceName = getPluginOptionName(pluginName, 'pageinstance');\nconst backoffTime = getPluginOptionName(pluginName, 'backoffTime');\nconst autosaveHasReset = getPluginOptionName(pluginName, 'autosaveHasReset');\n\nexport const register = (editor) => {\n const registerOption = editor.options.register;\n registerOption(initialisedOptionName, {\n processor: 'boolean',\n \"default\": false,\n });\n\n registerOption(pageHashName, {\n processor: 'string',\n \"default\": '',\n });\n\n registerOption(pageInstanceName, {\n processor: 'string',\n \"default\": '',\n });\n registerOption(pageInstanceName, {\n processor: 'string',\n \"default\": '',\n });\n registerOption(backoffTime, {\n processor: 'number',\n \"default\": 500,\n });\n registerOption(autosaveHasReset, {\n processor: 'boolean',\n \"default\": false,\n });\n};\n\nexport const isInitialised = (editor) => {\n if (!ensureEditorIsValid(editor)) {\n return false;\n }\n\n return editor.options.get(initialisedOptionName);\n};\nexport const markInitialised = (editor) => editor.options.set(initialisedOptionName, true);\nexport const getPageHash = (editor) => editor.options.get(pageHashName);\nexport const getPageInstance = (editor) => editor.options.get(pageInstanceName);\nexport const getBackoffTime = (editor) => editor.options.get(backoffTime);\nexport const setAutosaveHasReset = (editor) => editor.options.set(autosaveHasReset, true);\nexport const hasAutosaveHasReset = (editor) => editor.options.get(autosaveHasReset);\n\nexport {\n getContextId,\n getDraftItemId,\n};\n"],"names":["initialisedOptionName","pluginName","pageHashName","pageInstanceName","backoffTime","autosaveHasReset","editor","registerOption","options","register","processor","get","set"],"mappings":";;;;;;;;MA+BMA,uBAAwB,gCAAoBC,mBAAY,eACxDC,cAAe,gCAAoBD,mBAAY,YAC/CE,kBAAmB,gCAAoBF,mBAAY,gBACnDG,aAAc,gCAAoBH,mBAAY,eAC9CI,kBAAmB,gCAAoBJ,mBAAY,sCAEhCK,eACfC,eAAiBD,OAAOE,QAAQC,SACtCF,eAAeP,sBAAuB,CAClCU,UAAW,mBACA,IAGfH,eAAeL,aAAc,CACzBQ,UAAW,iBACA,KAGfH,eAAeJ,iBAAkB,CAC7BO,UAAW,iBACA,KAEfH,eAAeJ,iBAAkB,CAC7BO,UAAW,iBACA,KAEfH,eAAeH,YAAa,CACxBM,UAAW,iBACA,MAEfH,eAAeF,iBAAkB,CAC7BK,UAAW,mBACA,4BAIWJ,WACrB,8BAAoBA,SAIlBA,OAAOE,QAAQG,IAAIX,gDAEEM,QAAWA,OAAOE,QAAQI,IAAIZ,uBAAuB,wBACzDM,QAAWA,OAAOE,QAAQG,IAAIT,uCAC1BI,QAAWA,OAAOE,QAAQG,IAAIR,0CAC/BG,QAAWA,OAAOE,QAAQG,IAAIP,0CACzBE,QAAWA,OAAOE,QAAQI,IAAIP,kBAAkB,gCAChDC,QAAWA,OAAOE,QAAQG,IAAIN"}
@@ -0,0 +1,10 @@
define("tiny_autosave/plugin",["exports","editor_tiny/loader","editor_tiny/utils","./common","./options","./autosaver"],(function(_exports,_loader,_utils,_common,Options,Autosaver){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 Autosave plugin for Moodle.
*
* @module tiny_autosave/plugin
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,Options=_interopRequireWildcard(Options),Autosaver=_interopRequireWildcard(Autosaver);var _default=new Promise((async resolve=>{const[tinyMCE,pluginMetadata]=await Promise.all([(0,_loader.getTinyMCE)(),(0,_utils.getPluginMetadata)(_common.component,_common.pluginName)]);tinyMCE.PluginManager.add(_common.pluginName,(editor=>(Options.register(editor),Autosaver.register(editor),pluginMetadata))),resolve(_common.pluginName)}));return _exports.default=_default,_exports.default}));
//# sourceMappingURL=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 <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Autosave plugin for Moodle.\n *\n * @module tiny_autosave/plugin\n * @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>\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 Options from './options';\nimport * as Autosaver from './autosaver';\n\n// Setup the autosave Plugin.\n// eslint-disable-next-line no-async-promise-executor\nexport default new Promise(async(resolve) => {\n const [tinyMCE, pluginMetadata] = await Promise.all([\n getTinyMCE(),\n getPluginMetadata(component, pluginName),\n ]);\n\n // Note: The PluginManager.add function does not accept a Promise.\n // Any asynchronous code must be run before this point.\n tinyMCE.PluginManager.add(pluginName, (editor) => {\n // Register options.\n Options.register(editor);\n\n // Register the Autosaver.\n Autosaver.register(editor);\n\n return pluginMetadata;\n });\n\n resolve(pluginName);\n});\n"],"names":["Promise","async","tinyMCE","pluginMetadata","all","component","pluginName","PluginManager","add","editor","Options","register","Autosaver","resolve"],"mappings":";;;;;;;wLA+Be,IAAIA,SAAQC,MAAAA,gBAChBC,QAASC,sBAAwBH,QAAQI,IAAI,EAChD,yBACA,4BAAkBC,kBAAWC,sBAKjCJ,QAAQK,cAAcC,IAAIF,oBAAaG,SAEnCC,QAAQC,SAASF,QAGjBG,UAAUD,SAASF,QAEZN,kBAGXU,QAAQP"}
@@ -0,0 +1,10 @@
define("tiny_autosave/repository",["exports","core/ajax","core/config","./options","core/pending","editor_tiny/utils"],(function(_exports,_ajax,config,Options,_pending,_utils){var obj;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}
/**
* Repository helper for the Moodle Tiny Autosave plugin.
*
* @module tiny_autosave/repository
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.updateAutosaveSession=_exports.resumeAutosaveSession=_exports.removeAutosaveSession=void 0,config=_interopRequireWildcard(config),Options=_interopRequireWildcard(Options),_pending=(obj=_pending)&&obj.__esModule?obj:{default:obj};const fetchOne=(methodname,args)=>(0,_ajax.call)([{methodname:methodname,args:args}])[0];_exports.resumeAutosaveSession=editor=>{if(!(0,_utils.ensureEditorIsValid)(editor))return Promise.reject("Invalid editor");const pendingPromise=new _pending.default("tiny_autosave/repository:resumeAutosaveSession");return fetchOne("tiny_autosave_resume_session",{contextid:Options.getContextId(editor),pagehash:Options.getPageHash(editor),pageinstance:Options.getPageInstance(editor),elementid:editor.targetElm.id,draftid:Options.getDraftItemId(editor)}).then((result=>(pendingPromise.resolve(),result)))};_exports.updateAutosaveSession=editor=>{if(!(0,_utils.ensureEditorIsValid)(editor))return Promise.reject("Invalid editor");if(Options.hasAutosaveHasReset(editor))return Promise.reject("Skipping store of autosave content - content has been reset");const pendingPromise=new _pending.default("tiny_autosave/repository:updateAutosaveSession");return fetchOne("tiny_autosave_update_session",{contextid:Options.getContextId(editor),pagehash:Options.getPageHash(editor),pageinstance:Options.getPageInstance(editor),elementid:editor.targetElm.id,drafttext:editor.getContent()}).then((result=>(pendingPromise.resolve(),result)))};_exports.removeAutosaveSession=editor=>{if(!(0,_utils.ensureEditorIsValid)(editor))throw new Error("Invalid editor");Options.setAutosaveHasReset(editor);const requestUrl=new URL("".concat(config.wwwroot,"/lib/ajax/service.php"));requestUrl.searchParams.set("sesskey",config.sesskey);const args={contextid:Options.getContextId(editor),pagehash:Options.getPageHash(editor),pageinstance:Options.getPageInstance(editor),elementid:editor.targetElm.id};navigator.sendBeacon(requestUrl,JSON.stringify([{index:0,methodname:"tiny_autosave_reset_session",args:args}]))}}));
//# sourceMappingURL=repository.min.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,10 @@
define("tiny_autosave/storage",["exports","./repository","core/pending","./options","core/log","./common"],(function(_exports,Repository,_pending,_options,_log,_common){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}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)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.saveDraft=_exports.restoreDraft=_exports.removeAutosaveSession=void 0,Repository=function(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]}newObj.default=obj,cache&&cache.set(obj,newObj);return newObj}
/**
* Storage helper for the Moodle Tiny Autosave plugin.
*
* @module tiny_autosave/storage
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/(Repository),_pending=_interopRequireDefault(_pending),_log=_interopRequireDefault(_log);const saveDebounceMap=new Map;_exports.restoreDraft=async editor=>{const pendingPromise=new _pending.default("tiny_autosave/restoreDraft");try{const session=await Repository.resumeAutosaveSession(editor);session&&session.drafttext&&editor.undoManager.ignore((()=>{editor.setContent(session.drafttext),editor.save()}))}catch(error){_log.default.warn("Failed to restore draft: ".concat(error),(0,_common.getLogSource)(editor))}(0,_options.markInitialised)(editor),pendingPromise.resolve()};_exports.saveDraft=editor=>{const timerId=saveDebounceMap.get(editor);timerId&&clearTimeout(timerId),saveDebounceMap.set(editor,setTimeout((()=>{_log.default.debug("Saving draft",(0,_common.getLogSource)(editor)),Repository.updateAutosaveSession(editor).catch((error=>window.console.warn(error)))}),(0,_options.getBackoffTime)(editor)))};_exports.removeAutosaveSession=editor=>{_log.default.debug("Removing Autosave session",(0,_common.getLogSource)(editor)),Repository.removeAutosaveSession(editor)}}));
//# sourceMappingURL=storage.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"storage.min.js","sources":["../src/storage.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Storage helper for the Moodle Tiny Autosave plugin.\n *\n * @module tiny_autosave/storage\n * @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport * as Repository from \"./repository\";\nimport Pending from 'core/pending';\nimport {\n markInitialised,\n getBackoffTime,\n} from \"./options\";\nimport Log from 'core/log';\nimport {getLogSource} from './common';\n\n/** @property {Map} A map of debounced draft saves */\nconst saveDebounceMap = new Map();\n\n/**\n * Attempt to restore a draft into the editor\n *\n * @param {TinyMCE} editor The Editor to restore a draft for\n */\nexport const restoreDraft = async(editor) => {\n const pendingPromise = new Pending('tiny_autosave/restoreDraft');\n try {\n const session = await Repository.resumeAutosaveSession(editor);\n if (session && session.drafttext) {\n editor.undoManager.ignore(() => {\n editor.setContent(session.drafttext);\n editor.save();\n });\n }\n } catch (error) {\n // Ignore any errors as drafts are optional.\n Log.warn(`Failed to restore draft: ${error}`, getLogSource(editor));\n }\n markInitialised(editor);\n pendingPromise.resolve();\n};\n\n/**\n * Save the current content of the editor as a draft.\n *\n * @param {TinyMCE} editor\n */\nexport const saveDraft = (editor) => {\n const timerId = saveDebounceMap.get(editor);\n if (timerId) {\n clearTimeout(timerId);\n }\n saveDebounceMap.set(editor, setTimeout(() => {\n Log.debug(`Saving draft`, getLogSource(editor));\n Repository.updateAutosaveSession(editor)\n .catch((error) => window.console.warn(error));\n }, getBackoffTime(editor)));\n};\n\n/**\n * Delete the draft for the current editor.\n *\n * @param {TinyMCE} editor\n */\nexport const removeAutosaveSession = (editor) => {\n Log.debug(`Removing Autosave session`, getLogSource(editor));\n Repository.removeAutosaveSession(editor);\n};\n"],"names":["saveDebounceMap","Map","async","pendingPromise","Pending","session","Repository","resumeAutosaveSession","editor","drafttext","undoManager","ignore","setContent","save","error","warn","resolve","timerId","get","clearTimeout","set","setTimeout","debug","updateAutosaveSession","catch","window","console","removeAutosaveSession"],"mappings":";;;;;;;oGAiCMA,gBAAkB,IAAIC,0BAOAC,MAAAA,eAClBC,eAAiB,IAAIC,iBAAQ,wCAEzBC,cAAgBC,WAAWC,sBAAsBC,QACnDH,SAAWA,QAAQI,WACnBD,OAAOE,YAAYC,QAAO,KACtBH,OAAOI,WAAWP,QAAQI,WAC1BD,OAAOK,UAGjB,MAAOC,oBAEDC,wCAAiCD,QAAS,wBAAaN,sCAE/CA,QAChBL,eAAea,8BAQOR,eAChBS,QAAUjB,gBAAgBkB,IAAIV,QAChCS,SACAE,aAAaF,SAEjBjB,gBAAgBoB,IAAIZ,OAAQa,YAAW,kBAC/BC,sBAAsB,wBAAad,SACvCF,WAAWiB,sBAAsBf,QAChCgB,OAAOV,OAAUW,OAAOC,QAAQX,KAAKD,YACvC,2BAAeN,0CAQgBA,sBAC9Bc,mCAAmC,wBAAad,SACpDF,WAAWqB,sBAAsBnB"}
@@ -0,0 +1,82 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Storage helper for the Moodle Tiny Autosave plugin.
*
* @module tiny_autosave/autosaver
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import * as Options from './options';
import * as Storage from './storage';
import Log from 'core/log';
import {eventTypes} from 'core_form/events';
import {getLogSource} from './common';
export const register = (editor) => {
const undoHandler = () => {
if (!editor.undoManager.hasUndo()) {
Log.debug(`Ignoring undo event as there is no undo history`, getLogSource(editor));
return;
}
Storage.saveDraft(editor);
};
const visibilityChangedHandler = () => {
if (document.visibilityState === 'hidden') {
if (Options.isInitialised(editor)) {
Storage.saveDraft(editor);
}
}
};
// Javascript form submission handler.
const handleFormSubmittedByJavascript = (e) => {
if (Options.isInitialised(editor) && e.target.contains(editor.getElement())) {
removeAutoSaveSession();
}
};
// Remove the auto save session.
const removeAutoSaveSession = () => {
document.removeEventListener('visibilitychange', visibilityChangedHandler);
document.removeEventListener(eventTypes.formSubmittedByJavascript, handleFormSubmittedByJavascript);
Storage.removeAutosaveSession(editor);
};
// Attempt to store the draft one final time before the page unloads.
// Note: This may need to be sent as a beacon instead.
document.addEventListener('visibilitychange', visibilityChangedHandler);
// When the page is submitted as a form, remove the draft.
editor.on('submit', removeAutoSaveSession);
document.addEventListener(eventTypes.formSubmittedByJavascript, handleFormSubmittedByJavascript);
editor.on('init', () => {
// Setup the Undo handler.
editor.on('AddUndo', undoHandler);
if (editor.dom.isEmpty(editor.getBody())) {
Log.info(`Attempting to restore draft`, getLogSource(editor));
Storage.restoreDraft(editor);
} else {
// There was nothing to restore, so we can mark the editor as initialised.
Log.warn(`Skipping draft restoration. The editor is not empty.`, getLogSource(editor));
Options.markInitialised(editor);
}
});
};
@@ -0,0 +1,28 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Tiny Autosave plugin for Moodle.
*
* @module tiny_autosave/common
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
export default {
component: 'tiny_autosave',
pluginName: 'tiny_autosave/plugin',
getLogSource: (editor) => `tiny_autosave/${editor.id}`,
};
@@ -0,0 +1,85 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Options helper for the Moodle Tiny Autosave plugin.
*
* @module tiny_autosave/options
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {pluginName} from './common';
import {
getContextId,
getDraftItemId,
getPluginOptionName,
} from 'editor_tiny/options';
import {ensureEditorIsValid} from 'editor_tiny/utils';
const initialisedOptionName = getPluginOptionName(pluginName, 'initialised');
const pageHashName = getPluginOptionName(pluginName, 'pagehash');
const pageInstanceName = getPluginOptionName(pluginName, 'pageinstance');
const backoffTime = getPluginOptionName(pluginName, 'backoffTime');
const autosaveHasReset = getPluginOptionName(pluginName, 'autosaveHasReset');
export const register = (editor) => {
const registerOption = editor.options.register;
registerOption(initialisedOptionName, {
processor: 'boolean',
"default": false,
});
registerOption(pageHashName, {
processor: 'string',
"default": '',
});
registerOption(pageInstanceName, {
processor: 'string',
"default": '',
});
registerOption(pageInstanceName, {
processor: 'string',
"default": '',
});
registerOption(backoffTime, {
processor: 'number',
"default": 500,
});
registerOption(autosaveHasReset, {
processor: 'boolean',
"default": false,
});
};
export const isInitialised = (editor) => {
if (!ensureEditorIsValid(editor)) {
return false;
}
return editor.options.get(initialisedOptionName);
};
export const markInitialised = (editor) => editor.options.set(initialisedOptionName, true);
export const getPageHash = (editor) => editor.options.get(pageHashName);
export const getPageInstance = (editor) => editor.options.get(pageInstanceName);
export const getBackoffTime = (editor) => editor.options.get(backoffTime);
export const setAutosaveHasReset = (editor) => editor.options.set(autosaveHasReset, true);
export const hasAutosaveHasReset = (editor) => editor.options.get(autosaveHasReset);
export {
getContextId,
getDraftItemId,
};
@@ -0,0 +1,51 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Tiny Autosave plugin for Moodle.
*
* @module tiny_autosave/plugin
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @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 Options from './options';
import * as Autosaver from './autosaver';
// Setup the autosave Plugin.
// eslint-disable-next-line no-async-promise-executor
export default new Promise(async(resolve) => {
const [tinyMCE, pluginMetadata] = await Promise.all([
getTinyMCE(),
getPluginMetadata(component, pluginName),
]);
// Note: The PluginManager.add function does not accept a Promise.
// Any asynchronous code must be run before this point.
tinyMCE.PluginManager.add(pluginName, (editor) => {
// Register options.
Options.register(editor);
// Register the Autosaver.
Autosaver.register(editor);
return pluginMetadata;
});
resolve(pluginName);
});
@@ -0,0 +1,117 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Repository helper for the Moodle Tiny Autosave plugin.
*
* @module tiny_autosave/repository
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {call} from 'core/ajax';
import * as config from 'core/config';
import * as Options from './options';
import Pending from 'core/pending';
import {ensureEditorIsValid} from 'editor_tiny/utils';
const fetchOne = (methodname, args) => call([{
methodname,
args,
}])[0];
/**
* Resume an Autosave session.
*
* @param {TinyMCE} editor The TinyMCE editor instance
* @returns {Promise<AutosaveSession>} The Autosave session
*/
export const resumeAutosaveSession = (editor) => {
if (!ensureEditorIsValid(editor)) {
return Promise.reject('Invalid editor');
}
const pendingPromise = new Pending('tiny_autosave/repository:resumeAutosaveSession');
return fetchOne('tiny_autosave_resume_session', {
contextid: Options.getContextId(editor),
pagehash: Options.getPageHash(editor),
pageinstance: Options.getPageInstance(editor),
elementid: editor.targetElm.id,
draftid: Options.getDraftItemId(editor),
})
.then((result) => {
pendingPromise.resolve();
return result;
});
};
/**
* Update the content of the Autosave session.
*
* @param {TinyMCE} editor The TinyMCE editor instance
* @returns {Promise<AutosaveSession>} The Autosave session
*/
export const updateAutosaveSession = (editor) => {
if (!ensureEditorIsValid(editor)) {
return Promise.reject('Invalid editor');
}
if (Options.hasAutosaveHasReset(editor)) {
return Promise.reject('Skipping store of autosave content - content has been reset');
}
const pendingPromise = new Pending('tiny_autosave/repository:updateAutosaveSession');
return fetchOne('tiny_autosave_update_session', {
contextid: Options.getContextId(editor),
pagehash: Options.getPageHash(editor),
pageinstance: Options.getPageInstance(editor),
elementid: editor.targetElm.id,
drafttext: editor.getContent(),
})
.then((result) => {
pendingPromise.resolve();
return result;
});
};
/**
* Remove the Autosave session.
*
* @param {TinyMCE} editor The TinyMCE editor instance
*/
export const removeAutosaveSession = (editor) => {
if (!ensureEditorIsValid(editor)) {
throw new Error('Invalid editor');
}
Options.setAutosaveHasReset(editor);
// Please note that we must use a Beacon send here.
// The XHR is not guaranteed because it will be aborted on page transition.
// https://developer.mozilla.org/en-US/docs/Web/API/Beacon_API
// Note: Moodle does not currently have a sendBeacon API endpoint.
const requestUrl = new URL(`${config.wwwroot}/lib/ajax/service.php`);
requestUrl.searchParams.set('sesskey', config.sesskey);
const args = {
contextid: Options.getContextId(editor),
pagehash: Options.getPageHash(editor),
pageinstance: Options.getPageInstance(editor),
elementid: editor.targetElm.id,
};
navigator.sendBeacon(requestUrl, JSON.stringify([{
index: 0,
methodname: 'tiny_autosave_reset_session',
args,
}]));
};
@@ -0,0 +1,84 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Storage helper for the Moodle Tiny Autosave plugin.
*
* @module tiny_autosave/storage
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import * as Repository from "./repository";
import Pending from 'core/pending';
import {
markInitialised,
getBackoffTime,
} from "./options";
import Log from 'core/log';
import {getLogSource} from './common';
/** @property {Map} A map of debounced draft saves */
const saveDebounceMap = new Map();
/**
* Attempt to restore a draft into the editor
*
* @param {TinyMCE} editor The Editor to restore a draft for
*/
export const restoreDraft = async(editor) => {
const pendingPromise = new Pending('tiny_autosave/restoreDraft');
try {
const session = await Repository.resumeAutosaveSession(editor);
if (session && session.drafttext) {
editor.undoManager.ignore(() => {
editor.setContent(session.drafttext);
editor.save();
});
}
} catch (error) {
// Ignore any errors as drafts are optional.
Log.warn(`Failed to restore draft: ${error}`, getLogSource(editor));
}
markInitialised(editor);
pendingPromise.resolve();
};
/**
* Save the current content of the editor as a draft.
*
* @param {TinyMCE} editor
*/
export const saveDraft = (editor) => {
const timerId = saveDebounceMap.get(editor);
if (timerId) {
clearTimeout(timerId);
}
saveDebounceMap.set(editor, setTimeout(() => {
Log.debug(`Saving draft`, getLogSource(editor));
Repository.updateAutosaveSession(editor)
.catch((error) => window.console.warn(error));
}, getBackoffTime(editor)));
};
/**
* Delete the draft for the current editor.
*
* @param {TinyMCE} editor
*/
export const removeAutosaveSession = (editor) => {
Log.debug(`Removing Autosave session`, getLogSource(editor));
Repository.removeAutosaveSession(editor);
};
@@ -0,0 +1,267 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace tiny_autosave;
use stdClass;
/**
* Autosave Manager.
*
* @package tiny_autosave
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class autosave_manager {
/** @var int The contextid */
protected $contextid;
/** @var string The page hash reference */
protected $pagehash;
/** @var string The page instance reference */
protected $pageinstance;
/** @var string The elementid for this editor */
protected $elementid;
/** @var stdClass The user record */
protected $user;
/**
* Constructor for the autosave manager.
*
* @param int $contextid The contextid of the session
* @param string $pagehash The page hash
* @param string $pageinstance The page instance
* @param string $elementid The element id
* @param null|stdClass $user The user object for the owner of the autosave
*/
public function __construct(
int $contextid,
string $pagehash,
string $pageinstance,
string $elementid,
?stdClass $user = null
) {
global $USER;
$this->contextid = $contextid;
$this->pagehash = $pagehash;
$this->pageinstance = $pageinstance;
$this->elementid = $elementid;
$this->user = $user ?? $USER;
}
/**
* Get the autosave record for this session.
*
* @return stdClass|null
*/
public function get_autosave_record(): ?stdClass {
global $DB;
$record = $DB->get_record('tiny_autosave', [
'contextid' => $this->contextid,
'userid' => $this->user->id,
'pagehash' => $this->pagehash,
'elementid' => $this->elementid,
]);
if (empty($record)) {
return null;
}
return $record;
}
/**
* Create an autosave record for the session.
*
* @param string $drafttext The draft text to save
* @param null|int $draftid The draft file area if one is used
* @return stdClass The autosave record
*/
public function create_autosave_record(string $drafttext, ?int $draftid = null): stdClass {
global $DB;
$record = (object) [
'userid' => $this->user->id,
'contextid' => $this->contextid,
'pagehash' => $this->pagehash,
'pageinstance' => $this->pageinstance,
'elementid' => $this->elementid,
'drafttext' => $drafttext,
'timemodified' => time(),
];
if ($draftid) {
$record->draftid = $draftid;
}
$record->id = $DB->insert_record('tiny_autosave', $record);
return $record;
}
/**
* Update the text of the autosave session.
*
* @param string $drafttext The text to save
* @return stdClass The updated record
*/
public function update_autosave_record(string $drafttext): stdClass {
global $DB;
$record = $this->get_autosave_record();
if ($record) {
$record->drafttext = $drafttext;
$record->timemodified = time();
$DB->update_record('tiny_autosave', $record);
return $record;
} else {
return $this->create_autosave_record($drafttext);
}
}
/**
* Resume an autosave session, updating the draft file area if relevant.
*
* @param null|int $draftid The draft file area to update
* @return stdClass The updated autosave record
*/
public function resume_autosave_session(?int $draftid = null): stdClass {
$record = $this->get_autosave_record();
if (!$record) {
return $this->create_autosave_record('', $draftid);
}
if ($this->is_autosave_stale($record)) {
// If the autosave record it stale, remove it and create a new, blank, record.
$this->remove_autosave_record();
return $this->create_autosave_record('', $draftid);
}
if (empty($draftid)) {
// There is no file area to handle, so just return the record without any further changes.
return $record;
}
// This autosave is not stale, so update the draftid and move any files over to the new draft file area.
return $this->update_draftid_for_record($record, $draftid);
}
/**
* Check whether the autosave data is stale.
*
* Records are considered stale if either of the following conditions are true:
* - The record is older than the stale period
* - Any of the files in the draft area are newer than the autosave data itself
*
* @param stdClass $record The autosave record
* @return bool Whether the record is stale
*/
protected function is_autosave_stale(stdClass $record): bool {
$timemodified = $record->timemodified;
// TODO Create the UI for the stale period.
$staleperiod = get_config('tiny_autosave', 'staleperiod');
if (empty($staleperiod)) {
$staleperiod = (4 * DAYSECS);
}
$stale = $timemodified < (time() - $staleperiod);
if (empty($record->draftid)) {
return $stale;
}
$fs = get_file_storage();
$files = $fs->get_directory_files($record->contextid, 'user', 'draft', $record->draftid, '/', true, true);
$lastfilemodified = 0;
foreach ($files as $file) {
if ($record->timemodified < $file->get_timemodified()) {
$stale = true;
break;
}
}
return $stale;
}
/**
* Move the files relating to the autosave session to a new draft file area.
*
* @param stdClass $record The autosave record
* @param int $newdraftid The new draftid to move files to
* @return stdClass The updated autosave record
*/
protected function update_draftid_for_record(stdClass $record, int $newdraftid): stdClass {
global $CFG, $DB;
require_once("{$CFG->libdir}/filelib.php");
// Copy all draft files from the old draft area.
$usercontext = \context_user::instance($this->user->id);
// This function copies all the files in one draft area, to another area (in this case it's
// another draft area). It also rewrites the text to @@PLUGINFILE@@ links.
$record->drafttext = file_save_draft_area_files(
$record->draftid,
$usercontext->id,
'user',
'draft',
$newdraftid,
[],
$record->drafttext
);
// Final rewrite to the new draft area (convert the @@PLUGINFILES@@ again).
$record->drafttext = file_rewrite_pluginfile_urls(
$record->drafttext,
'draftfile.php',
$usercontext->id,
'user',
'draft',
$newdraftid
);
$record->draftid = $newdraftid;
$record->pageinstance = $this->pageinstance;
$record->timemodified = time();
$DB->update_record('tiny_autosave', $record);
return $record;
}
/**
* Remove the autosave record.
*/
public function remove_autosave_record(): void {
global $DB;
$DB->delete_records('tiny_autosave', [
'contextid' => $this->contextid,
'userid' => $this->user->id,
'pagehash' => $this->pagehash,
'elementid' => $this->elementid,
]);
}
}
@@ -0,0 +1,96 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace tiny_autosave\external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_value;
/**
* Web Service to reset the autosave session.
*
* @package tiny_autosave
* @category external
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class reset_autosave_session extends external_api {
/**
* Returns description of method parameters
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'contextid' => new external_value(PARAM_INT, 'The context id that owns the editor', VALUE_REQUIRED),
'pagehash' => new external_value(PARAM_ALPHANUMEXT, 'The page hash', VALUE_REQUIRED),
'pageinstance' => new external_value(PARAM_ALPHANUMEXT, 'The page instance', VALUE_REQUIRED),
'elementid' => new external_value(PARAM_RAW, 'The ID of the element', VALUE_REQUIRED),
]);
}
/**
* Reset the autosave entry for this autosave instance.
*
* If not matching autosave area could be found, the function will
* silently return and this is not treated as an error condition.
*
* @param int $contextid The context id of the owner
* @param string $pagehash The hash of the page
* @param string $pageinstance The instance id of the page
* @param string $elementid The id of the element
* @return null
*/
public static function execute(
int $contextid,
string $pagehash,
string $pageinstance,
string $elementid
): array {
[
'contextid' => $contextid,
'pagehash' => $pagehash,
'elementid' => $elementid,
'pageinstance' => $pageinstance,
] = self::validate_parameters(self::execute_parameters(), [
'contextid' => $contextid,
'pagehash' => $pagehash,
'elementid' => $elementid,
'pageinstance' => $pageinstance,
]);
// May have been called by a non-logged in user.
if (isloggedin() && !isguestuser()) {
$manager = new \tiny_autosave\autosave_manager($contextid, $pagehash, $pageinstance, $elementid);
$manager->remove_autosave_record();
}
return [];
}
/**
* Describe the return structure of the external service.
*
* @return external_single_structure
*/
public static function execute_returns(): external_single_structure {
return new external_single_structure([]);
}
}
@@ -0,0 +1,111 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace tiny_autosave\external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_value;
/**
* Web Service to resume an autosave session.
*
* @package tiny_autosave
* @category external
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class resume_autosave_session extends external_api {
/**
* Returns description of method parameters
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'contextid' => new external_value(PARAM_INT, 'The context id that owns the editor', VALUE_REQUIRED),
'pagehash' => new external_value(PARAM_ALPHANUMEXT, 'The page hash', VALUE_REQUIRED),
'pageinstance' => new external_value(PARAM_ALPHANUMEXT, 'The page instance', VALUE_REQUIRED),
'elementid' => new external_value(PARAM_RAW, 'The ID of the element', VALUE_REQUIRED),
'draftid' => new external_value(
PARAM_INT,
'The new draft item id to resume files to',
VALUE_REQUIRED,
null,
NULL_ALLOWED
),
]);
}
/**
* Reset the autosave entry for this autosave instance.
*
* If not matching autosave area could be found, the function will
* silently return and this is not treated as an error condition.
*
* @param int $contextid The context id of the owner
* @param string $pagehash The hash of the page
* @param string $pageinstance The instance id of the page
* @param string $elementid The id of the element
* @param int $draftid The id of the draftid to resume to
* @return null
*/
public static function execute(
int $contextid,
string $pagehash,
string $pageinstance,
string $elementid,
?int $draftid
): array {
[
'contextid' => $contextid,
'pagehash' => $pagehash,
'pageinstance' => $pageinstance,
'elementid' => $elementid,
'draftid' => $draftid,
] = self::validate_parameters(self::execute_parameters(), [
'contextid' => $contextid,
'pagehash' => $pagehash,
'pageinstance' => $pageinstance,
'elementid' => $elementid,
'draftid' => $draftid,
]);
$drafttext = '';
// May have been called by a non-logged in user.
if (isloggedin() && !isguestuser()) {
$manager = new \tiny_autosave\autosave_manager($contextid, $pagehash, $pageinstance, $elementid);
$drafttext = $manager->resume_autosave_session($draftid)->drafttext;
}
return [
'drafttext' => $drafttext,
];
}
/**
* Describe the return structure of the external service.
*
* @return external_single_structure
*/
public static function execute_returns(): external_single_structure {
return new external_single_structure([
'drafttext' => new external_value(PARAM_RAW, 'The draft text'),
]);
}
}
@@ -0,0 +1,100 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace tiny_autosave\external;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_value;
/**
* Web Service to update an autosave session's content.
*
* @package tiny_autosave
* @category external
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class update_autosave_session_content extends external_api {
/**
* Returns description of method parameters
*
* @return external_function_parameters
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'contextid' => new external_value(PARAM_INT, 'The context id that owns the editor', VALUE_REQUIRED),
'pagehash' => new external_value(PARAM_ALPHANUMEXT, 'The page hash', VALUE_REQUIRED),
'pageinstance' => new external_value(PARAM_ALPHANUMEXT, 'The page instance', VALUE_REQUIRED),
'elementid' => new external_value(PARAM_RAW, 'The ID of the element', VALUE_REQUIRED),
'drafttext' => new external_value(PARAM_RAW, 'The draft text', VALUE_REQUIRED),
]);
}
/**
* Reset the autosave entry for this autosave instance.
*
* If not matching autosave area could be found, the function will
* silently return and this is not treated as an error condition.
*
* @param int $contextid The context id of the owner
* @param string $pagehash The hash of the page
* @param string $pageinstance The instance id of the page
* @param string $elementid The id of the element
* @param string $drafttext The text to store
* @return null
*/
public static function execute(
int $contextid,
string $pagehash,
string $pageinstance,
string $elementid,
string $drafttext
): array {
[
'contextid' => $contextid,
'pagehash' => $pagehash,
'pageinstance' => $pageinstance,
'elementid' => $elementid,
'drafttext' => $drafttext,
] = self::validate_parameters(self::execute_parameters(), [
'contextid' => $contextid,
'pagehash' => $pagehash,
'pageinstance' => $pageinstance,
'elementid' => $elementid,
'drafttext' => $drafttext,
]);
// May have been called by a non-logged in user.
if (isloggedin() && !isguestuser()) {
$manager = new \tiny_autosave\autosave_manager($contextid, $pagehash, $pageinstance, $elementid);
$manager->update_autosave_record($drafttext);
}
return [];
}
/**
* Describe the return structure of the external service.
*
* @return external_single_structure
*/
public static function execute_returns(): external_single_structure {
return new external_single_structure([]);
}
}
@@ -0,0 +1,52 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace tiny_autosave;
use context;
use editor_tiny\plugin;
use editor_tiny\plugin_with_configuration;
/**
* Tiny autosave plugin for Moodle.
*
* @package tiny_autosave
* @copyright 2021 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class plugininfo extends plugin implements plugin_with_configuration {
public static function get_plugin_configuration_for_context(
context $context,
array $options,
array $fpoptions,
?\editor_tiny\editor $editor = null
): array {
global $PAGE;
if (empty($editor) || empty($options['autosave'])) {
return [
'autosave' => null,
];
}
return [
'pagehash' => sha1($PAGE->url . '<>' . s($editor->get_text())),
'pageinstance' => bin2hex(random_bytes(16)),
'backoffTime' => (defined('BEHAT_SITE_RUNNING') && BEHAT_SITE_RUNNING) ? 0 : 500,
];
}
}
@@ -0,0 +1,237 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace tiny_autosave\privacy;
use core_privacy\local\request\approved_contextlist;
use core_privacy\local\request\writer;
use core_privacy\local\metadata\collection;
use core_privacy\local\request\userlist;
use core_privacy\local\request\approved_userlist;
use stdClass;
/**
* Privacy Subsystem implementation for tiny_autosave.
*
* @package tiny_autosave
* @copyright 2022 Andrew Nicols <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements
// The tiny editor stores user provided data.
\core_privacy\local\metadata\provider,
// The tiny editor provides data directly to core.
\core_privacy\local\request\plugin\provider,
// The tiny editor is capable of determining which users have data within it.
\core_privacy\local\request\core_userlist_provider {
/**
* Returns information about how tiny_autosave stores its data.
*
* @param collection $collection The initialised collection to add items to.
* @return collection A listing of user data stored through this system.
*/
public static function get_metadata(collection $collection): collection {
// There isn't much point giving details about the pageid, etc.
$collection->add_database_table('tiny_autosave', [
'userid' => 'privacy:metadata:database:tiny_autosave:userid',
'drafttext' => 'privacy:metadata:database:tiny_autosave:drafttext',
'timemodified' => 'privacy:metadata:database:tiny_autosave:timemodified',
], 'privacy:metadata:database:tiny_autosave');
return $collection;
}
/**
* Get the list of contexts that contain user information for the specified user.
*
* @param int $userid The user to search.
* @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
*/
public static function get_contexts_for_userid(int $userid): \core_privacy\local\request\contextlist {
$contextlist = new \core_privacy\local\request\contextlist();
// Data may be saved in the user context.
$sql = "SELECT
c.id
FROM {tiny_autosave} eas
JOIN {context} c ON c.id = eas.contextid
WHERE contextlevel = :contextuser AND c.instanceid = :userid";
$contextlist->add_from_sql($sql, ['contextuser' => CONTEXT_USER, 'userid' => $userid]);
// Data may be saved against the userid.
$sql = "SELECT contextid FROM {tiny_autosave} WHERE userid = :userid";
$contextlist->add_from_sql($sql, ['userid' => $userid]);
return $contextlist;
}
/**
* Get the list of users within a specific context.
*
* @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
*/
public static function get_users_in_context(userlist $userlist) {
$context = $userlist->get_context();
$params = [
'contextid' => $context->id
];
$sql = "SELECT userid
FROM {tiny_autosave}
WHERE contextid = :contextid";
$userlist->add_from_sql('userid', $sql, $params);
}
/**
* Export all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts to export information for.
*/
public static function export_user_data(approved_contextlist $contextlist) {
global $DB;
$user = $contextlist->get_user();
// Firstly export all autosave records from all contexts in the list owned by the given user.
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
$contextparams['userid'] = $user->id;
$sql = "SELECT *
FROM {tiny_autosave}
WHERE userid = :userid AND contextid {$contextsql}";
$autosaves = $DB->get_recordset_sql($sql, $contextparams);
self::export_autosaves($user, $autosaves);
// Additionally export all eventual records in the given user's context regardless the actual owner.
// We still consider them to be the user's personal data even when edited by someone else.
[$contextsql, $contextparams] = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
$contextparams['userid'] = $user->id;
$contextparams['contextuser'] = CONTEXT_USER;
$sql = "SELECT eas.*
FROM {tiny_autosave} eas
JOIN {context} c ON c.id = eas.contextid
WHERE c.id {$contextsql} AND c.contextlevel = :contextuser AND c.instanceid = :userid";
$autosaves = $DB->get_recordset_sql($sql, $contextparams);
self::export_autosaves($user, $autosaves);
}
/**
* Export all autosave records in the recordset, and close the recordset when finished.
*
* @param stdClass $user The user whose data is to be exported
* @param \moodle_recordset $autosaves The recordset containing the data to export
*/
protected static function export_autosaves(stdClass $user, \moodle_recordset $autosaves) {
foreach ($autosaves as $autosave) {
$context = \context::instance_by_id($autosave->contextid);
$subcontext = [
get_string('pluginname', 'tiny_autosave'),
$autosave->id,
];
$html = writer::with_context($context)
->rewrite_pluginfile_urls($subcontext, 'user', 'draft', $autosave->draftid, $autosave->drafttext);
$data = (object) [
'drafttext' => format_text($html, FORMAT_HTML, static::get_filter_options()),
'timemodified' => \core_privacy\local\request\transform::datetime($autosave->timemodified),
];
if ($autosave->userid != $user->id) {
$data->author = \core_privacy\local\request\transform::user($autosave->userid);
}
writer::with_context($context)
->export_data($subcontext, $data)
->export_area_files($subcontext, 'user', 'draft', $autosave->draftid);
}
$autosaves->close();
}
/**
* Delete all data for all users in the specified context.
*
* @param \context $context The specific context to delete data for.
*/
public static function delete_data_for_all_users_in_context(\context $context) {
global $DB;
$DB->delete_records('tiny_autosave', [
'contextid' => $context->id,
]);
}
/**
* Delete multiple users within a single context.
*
* @param approved_userlist $userlist The approved context and user information to delete information for.
*/
public static function delete_data_for_users(approved_userlist $userlist) {
global $DB;
$context = $userlist->get_context();
$userids = $userlist->get_userids();
[$useridsql, $useridsqlparams] = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
$params = ['contextid' => $context->id] + $useridsqlparams;
$DB->delete_records_select('tiny_autosave', "contextid = :contextid AND userid {$useridsql}",
$params);
}
/**
* Delete all user data for the specified user, in the specified contexts.
*
* @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
*/
public static function delete_data_for_user(approved_contextlist $contextlist) {
global $DB;
$user = $contextlist->get_user();
[$contextsql, $contextparams] = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
$contextparams['userid'] = $user->id;
$sql = "SELECT * FROM {tiny_autosave} WHERE contextid {$contextsql}";
$autosaves = $DB->delete_records_select(
'tiny_autosave',
"userid = :userid AND contextid {$contextsql}",
$contextparams
);
}
/**
* Get the filter options.
*
* This is shared to allow unit testing too.
*
* @return stdClass
*/
public static function get_filter_options() {
return (object) [
'overflowdiv' => true,
'noclean' => true,
];
}
}
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" ?>
<XMLDB PATH="lib/editor/tiny/plugins/autosave/db" VERSION="20140819" COMMENT="XMLDB file for Moodle tiny_autosave"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../../../lib/xmldb/xmldb.xsd"
>
<TABLES>
<TABLE NAME="tiny_autosave" COMMENT="The content of the textarea saved during autosave operations">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="elementid" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" COMMENT="The unique id for the text editor in the form."/>
<FIELD NAME="contextid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The contextid that the form was loaded with."/>
<FIELD NAME="pagehash" TYPE="char" LENGTH="64" NOTNULL="true" SEQUENCE="false" COMMENT="The HTML DOM id of the page that loaded the form."/>
<FIELD NAME="userid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The id of the user that loaded the form."/>
<FIELD NAME="drafttext" TYPE="text" NOTNULL="true" SEQUENCE="false" COMMENT="The draft text"/>
<FIELD NAME="draftid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Optional draft area id containing draft files."/>
<FIELD NAME="pageinstance" TYPE="char" LENGTH="64" NOTNULL="true" SEQUENCE="false" COMMENT="The browser tab instance that last saved the draft text. This is to prevent multiple tabs from the same user saving different text alternately."/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Store the last modified time for the auto save text."/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="autosave_uniq_key" TYPE="unique" FIELDS="elementid, contextid, userid, pagehash" COMMENT="Unique key for the user in the form in the page."/>
</KEYS>
</TABLE>
</TABLES>
</XMLDB>
@@ -0,0 +1,52 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* External service definitions for tiny_autosavse.
*
* @package tiny_autosave
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
$functions = [
'tiny_autosave_resume_session' => array(
'classname' => 'tiny_autosave\external\resume_autosave_session',
'methodname' => 'execute',
'description' => 'Resume an autosave session',
'type' => 'write',
'loginrequired' => false,
'ajax' => true,
),
'tiny_autosave_reset_session' => array(
'classname' => 'tiny_autosave\external\reset_autosave_session',
'methodname' => 'execute',
'description' => 'Reset an autosave session',
'type' => 'write',
'loginrequired' => false,
'ajax' => true,
),
'tiny_autosave_update_session' => array(
'classname' => 'tiny_autosave\external\update_autosave_session_content',
'methodname' => 'execute',
'description' => 'Update an autosave session',
'type' => 'write',
'loginrequired' => false,
'ajax' => true,
),
];
@@ -0,0 +1,30 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* Strings for component 'tiny_autosave', language 'en'.
*
* @package tiny_autosave
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['pluginname'] = 'Autosave';
$string['helplinktext'] = 'Autosave';
$string['privacy:metadata:database:tiny_autosave:userid'] = 'The user ID of the user who created the autosave session';
$string['privacy:metadata:database:tiny_autosave:drafttext'] = 'The text content of the autosave session';
$string['privacy:metadata:database:tiny_autosave:timemodified'] = 'The time that the autosave session was last modified';
$string['privacy:metadata:database:tiny_autosave'] = 'A table storing autosave session data for the TinyMCE editor';
@@ -0,0 +1,80 @@
@editor @editor_tiny @tiny_autosave
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 |
@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 the following "user private file" exists:
| user | teacher2 |
| filepath | lib/editor/tiny/tests/behat/fixtures/tinyscreenshot.png |
And 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 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 "<p>Image test</p>"
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 "How would you describe this image to someone who can't see it?" to "It's the Moodle"
And I click on "Save" "button" in the "Image details" "dialogue"
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 "<p>Modified text</p>"
@javascript
Scenario: Draft should not be restored if the form was submitted via Javascript
Given I am on the "Course 1" course page logged in as teacher1
And I follow "Calendar" in the user menu
And I click on "New event" "button"
And I click on "Show more..." "link" in the "New event" "dialogue"
And I set the field "Event title" to "Test course event"
And I set the field "Description" to "This is my draft"
And I click on "Save" "button"
And I click on "New event" "button"
When I click on "Show more..." "link" in the "New event" "dialogue"
Then the field "Description" matches value ""
@@ -0,0 +1,29 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* Tiny text editor Autosave Plugin version file.
*
* @package tiny_autosave
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->component = 'tiny_autosave';
@@ -0,0 +1,3 @@
define("tiny_equation/commands",["exports","editor_tiny/utils","core/str","tiny_equation/common","tiny_equation/ui","tiny_equation/equation","tiny_equation/options"],(function(_exports,_utils,_str,_common,_ui,_equation,_options){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getSetup=void 0;_exports.getSetup=async()=>{const[buttonText,buttonImage]=await Promise.all([(0,_str.getString)("buttontitle",_common.component),(0,_utils.getButtonImage)("icon",_common.component)]);return editor=>{(0,_options.isTexFilterActive)(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=>{editor.on("NodeChange",(()=>{const result=(0,_equation.getSelectedEquation)(editor);api.setActive(result)}))}}),editor.ui.registry.addMenuItem(_common.buttonName,{icon:_common.icon,text:buttonText,onAction:()=>(0,_ui.handleAction)(editor)}))}}}));
//# sourceMappingURL=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 <http://www.gnu.org/licenses/>.\n\nimport {getButtonImage} from 'editor_tiny/utils';\nimport {getString} from 'core/str';\nimport {component, buttonName, icon} from 'tiny_equation/common';\nimport {handleAction} from 'tiny_equation/ui';\nimport {getSelectedEquation} from 'tiny_equation/equation';\nimport {isTexFilterActive} from 'tiny_equation/options';\n\n/**\n * Tiny Equation commands.\n *\n * @module tiny_equation/commands\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\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 (isTexFilterActive(editor)) {\n // Register the Equation 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 Equation element it will show as toggled on.\n editor.ui.registry.addToggleButton(buttonName, {\n icon,\n tooltip: buttonText,\n onAction: () => {\n handleAction(editor);\n },\n onSetup: (api) => {\n editor.on('NodeChange', () => {\n const result = getSelectedEquation(editor);\n api.setActive(result);\n });\n },\n });\n\n // Add the Equation 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};\n"],"names":["async","buttonText","buttonImage","Promise","all","component","editor","ui","registry","addIcon","icon","html","addToggleButton","buttonName","tooltip","onAction","onSetup","api","on","result","setActive","addMenuItem","text"],"mappings":"wUA8BwBA,gBAEhBC,WACAC,mBACMC,QAAQC,IAAI,EAClB,kBAAU,cAAeC,oBACzB,yBAAe,OAAQA,4BAGnBC,UACA,8BAAkBA,UAElBA,OAAOC,GAAGC,SAASC,QAAQC,aAAMR,YAAYS,MAI7CL,OAAOC,GAAGC,SAASI,gBAAgBC,mBAAY,CAC3CH,KAAAA,aACAI,QAASb,WACTc,SAAU,0BACOT,SAEjBU,QAAUC,MACNX,OAAOY,GAAG,cAAc,WACdC,QAAS,iCAAoBb,QACnCW,IAAIG,UAAUD,cAO1Bb,OAAOC,GAAGC,SAASa,YAAYR,mBAAY,CACvCH,KAAAA,aACAY,KAAMrB,WACNc,SAAU,KAAM,oBAAaT"}
@@ -0,0 +1,3 @@
define("tiny_equation/common",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={pluginName:"tiny_equation/plugin",component:"tiny_equation",buttonName:"tiny_equation",icon:"tiny_equation"},_exports.default}));
//# sourceMappingURL=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 <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Equation common values.\n *\n * @module tiny_equation/common\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n pluginName: 'tiny_equation/plugin',\n component: 'tiny_equation',\n buttonName: 'tiny_equation',\n icon: 'tiny_equation',\n};\n"],"names":["pluginName","component","buttonName","icon"],"mappings":"sKAuBe,CACXA,WAAY,uBACZC,UAAW,gBACXC,WAAY,gBACZC,KAAM"}
@@ -0,0 +1,3 @@
define("tiny_equation/configuration",["exports","tiny_equation/common","editor_tiny/utils"],(function(_exports,_common,_utils){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.configure=void 0;_exports.configure=instanceConfig=>{return{menu:(0,_utils.addMenubarItem)(instanceConfig.menu,"insert",_common.component),toolbar:(toolbar=instanceConfig.toolbar,(0,_utils.addToolbarSection)(toolbar,"advanced","lists",!0),(0,_utils.addToolbarButton)(toolbar,"advanced",_common.component))};var toolbar}}));
//# sourceMappingURL=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 <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Equation configuration.\n *\n * @module tiny_equation/configuration\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {component as buttonName} from 'tiny_equation/common';\nimport {\n addMenubarItem,\n addToolbarButton,\n addToolbarSection,\n} from 'editor_tiny/utils';\n\nconst configureToolbar = (toolbar) => {\n addToolbarSection(toolbar, 'advanced', 'lists', true);\n return addToolbarButton(toolbar, 'advanced', buttonName);\n};\n\nexport const configure = (instanceConfig) => {\n // Update the instance configuration to add the Equation menu option to the menus and toolbars.\n return {\n menu: addMenubarItem(instanceConfig.menu, 'insert', buttonName),\n toolbar: configureToolbar(instanceConfig.toolbar),\n };\n};\n"],"names":["instanceConfig","menu","buttonName","toolbar"],"mappings":"oOAmC0BA,uBAEf,CACHC,MAAM,yBAAeD,eAAeC,KAAM,SAAUC,mBACpDC,SATkBA,QASQH,eAAeG,qCAR3BA,QAAS,WAAY,SAAS,IACzC,2BAAiBA,QAAS,WAAYD,qBAFvBC,IAAAA"}
@@ -0,0 +1,10 @@
define("tiny_equation/equation",["exports","tiny_equation/selectors"],(function(_exports,_selectors){var obj;
/**
* Equation helper for Tiny Equation plugin.
*
* @module tiny_equation/equation
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setEquation=_exports.getSourceEquation=_exports.getSelectedEquation=_exports.getCurrentEquationData=void 0,_selectors=(obj=_selectors)&&obj.__esModule?obj:{default:obj};let sourceEquation=null;const getSourceEquation=()=>sourceEquation;_exports.getSourceEquation=getSourceEquation;const getSelectedEquation=editor=>{const currentSelection=editor.selection.getSel();if(!currentSelection)return!1;const textSelection=editor.selection.getNode().textContent,currentCaretPos=currentSelection.focusOffset;let returnValue=!1;return _selectors.default.equationPatterns.forEach((pattern=>{const regexPattern=new RegExp(pattern.source,"g");[...textSelection.matchAll(regexPattern)].forEach((matches=>{const match=matches[0];let startIndex=0;const startOuter=textSelection.indexOf(match,startIndex),endOuter=startOuter+match.length,innerMatch=match.match(pattern);if(innerMatch&&innerMatch.length){const startInner=textSelection.indexOf(innerMatch[1],startOuter),endInner=startInner+innerMatch[1].length;if(currentCaretPos>=startOuter&&currentCaretPos<=endOuter)return returnValue=innerMatch[1],void(sourceEquation={startInnerPosition:startInner,endInnerPosition:endInner,innerMatch:innerMatch})}startIndex=endOuter}))})),!1!==returnValue?returnValue=returnValue.trim():sourceEquation=null,returnValue};_exports.getSelectedEquation=getSelectedEquation;_exports.getCurrentEquationData=editor=>{let properties={};const equation=getSelectedEquation(editor);return equation&&(properties.equation=equation),properties};_exports.setEquation=(currentForm,editor)=>{const input=currentForm.querySelector(_selectors.default.elements.equationTextArea),sourceEquation=getSourceEquation();let value=input.value;if(""!==value)if(sourceEquation){const selectedNode=editor.selection.getNode(),text=selectedNode.textContent;value=" "+value+" ",selectedNode.textContent=text.slice(0,sourceEquation.startInnerPosition)+value+text.slice(sourceEquation.endInnerPosition)}else value=_selectors.default.delimiters.start+" "+value+" "+_selectors.default.delimiters.end,editor.insertContent(value)}}));
//# sourceMappingURL=equation.min.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,3 @@
define("tiny_equation/modal",["exports","core/modal"],(function(_exports,_modal){var obj;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}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=(obj=_modal)&&obj.__esModule?obj:{default:obj};class EquationModal extends _modal.default{registerEventListeners(){super.registerEventListeners(),this.registerCloseOnSave(),this.registerCloseOnCancel()}configure(modalConfig){modalConfig.show=!0,modalConfig.large=!0,modalConfig.removeOnClose=!0,super.configure(modalConfig)}}return _exports.default=EquationModal,_defineProperty(EquationModal,"TYPE","tiny_equation/modal"),_defineProperty(EquationModal,"TEMPLATE","tiny_equation/modal"),_exports.default}));
//# sourceMappingURL=modal.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"modal.min.js","sources":["../src/modal.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Equation Modal for Tiny.\n *\n * @module tiny_equation/modal\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Modal from 'core/modal';\n\nexport default class EquationModal extends Modal {\n static TYPE = 'tiny_equation/modal';\n static TEMPLATE = 'tiny_equation/modal';\n\n registerEventListeners() {\n // Call the parent registration.\n super.registerEventListeners();\n\n // Register to close on save/cancel.\n this.registerCloseOnSave();\n this.registerCloseOnCancel();\n }\n\n configure(modalConfig) {\n modalConfig.show = true;\n modalConfig.large = true;\n modalConfig.removeOnClose = true;\n\n super.configure(modalConfig);\n }\n}\n"],"names":["EquationModal","Modal","registerEventListeners","registerCloseOnSave","registerCloseOnCancel","configure","modalConfig","show","large","removeOnClose"],"mappings":"yYAyBqBA,sBAAsBC,eAIvCC,+BAEUA,8BAGDC,2BACAC,wBAGTC,UAAUC,aACNA,YAAYC,MAAO,EACnBD,YAAYE,OAAQ,EACpBF,YAAYG,eAAgB,QAEtBJ,UAAUC,oEAlBHN,qBACH,uCADGA,yBAEC"}
@@ -0,0 +1,11 @@
define("tiny_equation/options",["exports","editor_tiny/options","tiny_equation/common"],(function(_exports,_options,_common){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.register=_exports.isTexFilterActive=_exports.getTexDocsUrl=_exports.getLibraries=_exports.getContextId=void 0;
/**
* Options helper for Tiny Equation plugin.
*
* @module tiny_equation/options
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const librariesName=(0,_options.getPluginOptionName)(_common.pluginName,"libraries"),texFilterName=(0,_options.getPluginOptionName)(_common.pluginName,"texfilter"),contextIdName=(0,_options.getPluginOptionName)(_common.pluginName,"contextid"),texDocsUrlName=(0,_options.getPluginOptionName)(_common.pluginName,"texdocsurl");_exports.register=editor=>{const registerOption=editor.options.register;registerOption(librariesName,{processor:"array",default:[]}),registerOption(texFilterName,{processor:"boolean",default:!1}),registerOption(contextIdName,{processor:"number",default:0}),registerOption(texDocsUrlName,{processor:"string",default:""})};_exports.getLibraries=editor=>editor.options.get(librariesName);_exports.isTexFilterActive=editor=>editor.options.get(texFilterName);_exports.getContextId=editor=>editor.options.get(contextIdName);_exports.getTexDocsUrl=editor=>editor.options.get(texDocsUrlName)}));
//# sourceMappingURL=options.min.js.map
@@ -0,0 +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 <http://www.gnu.org/licenses/>.\n\n/**\n * Options helper for Tiny Equation plugin.\n *\n * @module tiny_equation/options\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\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 'tiny_equation/common';\n\nconst librariesName = getPluginOptionName(pluginName, 'libraries');\nconst texFilterName = getPluginOptionName(pluginName, 'texfilter');\nconst contextIdName = getPluginOptionName(pluginName, 'contextid');\nconst texDocsUrlName = getPluginOptionName(pluginName, 'texdocsurl');\n\n/**\n * Register the options for the Tiny Equation plugin.\n *\n * @param {TinyMCE} editor\n */\nexport const register = (editor) => {\n const registerOption = editor.options.register;\n\n registerOption(librariesName, {\n processor: 'array',\n \"default\": [],\n });\n\n registerOption(texFilterName, {\n processor: 'boolean',\n \"default\": false,\n });\n\n registerOption(contextIdName, {\n processor: 'number',\n \"default\": 0,\n });\n\n registerOption(texDocsUrlName, {\n processor: 'string',\n \"default\": '',\n });\n};\n\n/**\n * Get the libraries configuration for the Tiny Equation plugin.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getLibraries = (editor) => editor.options.get(librariesName);\n/**\n * Check if the TEX filter is active or not for the Tiny Equation plugin.\n *\n * @param {TinyMCE} editor\n * @returns {boolean}\n */\nexport const isTexFilterActive = (editor) => editor.options.get(texFilterName);\n/**\n * Get the context id for the Tiny Equation plugin.\n *\n * @param {TinyMCE} editor\n * @returns {number}\n */\nexport const getContextId = (editor) => editor.options.get(contextIdName);\n/**\n * Get the Tex Docs Url for the Tiny Equation plugin.\n *\n * @param {TinyMCE} editor\n * @returns {string}\n */\nexport const getTexDocsUrl = (editor) => editor.options.get(texDocsUrlName);\n"],"names":["librariesName","pluginName","texFilterName","contextIdName","texDocsUrlName","editor","registerOption","options","register","processor","get"],"mappings":";;;;;;;;MA0BMA,eAAgB,gCAAoBC,mBAAY,aAChDC,eAAgB,gCAAoBD,mBAAY,aAChDE,eAAgB,gCAAoBF,mBAAY,aAChDG,gBAAiB,gCAAoBH,mBAAY,gCAO9BI,eACfC,eAAiBD,OAAOE,QAAQC,SAEtCF,eAAeN,cAAe,CAC1BS,UAAW,gBACA,KAGfH,eAAeJ,cAAe,CAC1BO,UAAW,mBACA,IAGfH,eAAeH,cAAe,CAC1BM,UAAW,iBACA,IAGfH,eAAeF,eAAgB,CAC3BK,UAAW,iBACA,4BAUUJ,QAAWA,OAAOE,QAAQG,IAAIV,0CAOzBK,QAAWA,OAAOE,QAAQG,IAAIR,qCAOnCG,QAAWA,OAAOE,QAAQG,IAAIP,sCAO7BE,QAAWA,OAAOE,QAAQG,IAAIN"}
@@ -0,0 +1,10 @@
define("tiny_equation/plugin",["exports","editor_tiny/loader","editor_tiny/utils","tiny_equation/common","tiny_equation/commands","tiny_equation/configuration","tiny_equation/options"],(function(_exports,_loader,_utils,_common,Commands,Configuration,Options){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 Equation plugin for Moodle.
*
* @module tiny_equation/plugin
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @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),Options=_interopRequireWildcard(Options);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=>(Options.register(editor),setupCommands(editor),pluginMetadata))),resolve(["".concat(_common.component,"/plugin"),Configuration])}));return _exports.default=_default,_exports.default}));
//# sourceMappingURL=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 <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Equation plugin for Moodle.\n *\n * @module tiny_equation/plugin\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getTinyMCE} from 'editor_tiny/loader';\nimport {getPluginMetadata} from 'editor_tiny/utils';\n\nimport {component, pluginName} from 'tiny_equation/common';\nimport * as Commands from 'tiny_equation/commands';\nimport * as Configuration from 'tiny_equation/configuration';\nimport * as Options from 'tiny_equation/options';\n\n// eslint-disable-next-line no-async-promise-executor\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 // Register options.\n Options.register(editor);\n\n // Setup the Commands (buttons, menu items, and so on).\n setupCommands(editor);\n\n return pluginMetadata;\n });\n\n // Resolve the Equation 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","Options","register","resolve","Configuration"],"mappings":";;;;;;;2OAgCe,IAAIA,SAAQC,MAAAA,gBAEnBC,QACAC,cACAC,sBACMJ,QAAQK,IAAI,EAClB,wBACAC,SAASC,YACT,4BAAkBC,kBAAWC,sBAGjCP,QAAQQ,cAAcC,cAAOH,8BAAqBI,SAE9CC,QAAQC,SAASF,QAGjBT,cAAcS,QAEPR,kBAIXW,QAAQ,WAAIP,6BAAoBQ"}
@@ -0,0 +1,10 @@
define("tiny_equation/repository",["exports","core/ajax"],(function(_exports,_ajax){var obj;
/**
* A javascript module to handle TinyMCE Equation ajax actions.
*
* @module tiny_equation/repository
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.filterEquation=void 0,_ajax=(obj=_ajax)&&obj.__esModule?obj:{default:obj};_exports.filterEquation=(contextId,content)=>{const request={methodname:"tiny_equation_filter",args:{contextid:contextId,content:content}};return _ajax.default.call([request])[0]}}));
//# sourceMappingURL=repository.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"repository.min.js","sources":["../src/repository.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * A javascript module to handle TinyMCE Equation ajax actions.\n *\n * @module tiny_equation/repository\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport Ajax from 'core/ajax';\n\n/**\n * Filter the equation for given content.\n *\n * @param {Number} contextId The context id\n * @param {String} content Content to filter\n * @return {promise}\n */\nexport const filterEquation = (contextId, content) => {\n const request = {\n methodname: 'tiny_equation_filter',\n args: {\n contextid: contextId,\n content: content,\n }\n };\n\n return Ajax.call([request])[0];\n};\n"],"names":["contextId","content","request","methodname","args","contextid","Ajax","call"],"mappings":";;;;;;;wKA+B8B,CAACA,UAAWC,iBAChCC,QAAU,CACZC,WAAY,uBACZC,KAAM,CACFC,UAAWL,UACXC,QAASA,iBAIVK,cAAKC,KAAK,CAACL,UAAU"}
@@ -0,0 +1,3 @@
define("tiny_equation/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={equationPatterns:[/\$\$([\S\s]+?)\$\$/,/\\\(([\S\s]+?)\\\)/,/\\\[([\S\s]+?)\\\]/,/\[tex\]([\S\s]+?)\[\/tex\]/],delimiters:{start:"\\(",end:"\\)"},cursorLatex:"\\Downarrow ",actions:{submit:'[data-action="save"]'},elements:{form:"form",library:".tiny_equation_library",libraryItem:".tiny_equation_library button",equationTextArea:".tiny_equation_equation",preview:".tiny_equation_preview"}},_exports.default}));
//# sourceMappingURL=selectors.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"selectors.min.js","sources":["../src/selectors.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Equation plugin helper function to build queryable data selectors.\n *\n * @module tiny_equation/selectors\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n equationPatterns: [\n // We use space or not space because . does not match new lines.\n // $$ blah $$.\n /\\$\\$([\\S\\s]+?)\\$\\$/,\n // E.g. \"\\( blah \\)\".\n /\\\\\\(([\\S\\s]+?)\\\\\\)/,\n // E.g. \"\\[ blah \\]\".\n /\\\\\\[([\\S\\s]+?)\\\\\\]/,\n // E.g. \"[tex] blah [/tex]\".\n /\\[tex\\]([\\S\\s]+?)\\[\\/tex\\]/\n ],\n delimiters: {\n start: '\\\\(',\n end: '\\\\)'\n },\n cursorLatex: '\\\\Downarrow ',\n actions: {\n submit: '[data-action=\"save\"]'\n },\n elements: {\n form: 'form',\n library: '.tiny_equation_library',\n libraryItem: '.tiny_equation_library button',\n equationTextArea: '.tiny_equation_equation',\n preview: '.tiny_equation_preview',\n }\n};\n"],"names":["equationPatterns","delimiters","start","end","cursorLatex","actions","submit","elements","form","library","libraryItem","equationTextArea","preview"],"mappings":"yKAuBe,CACXA,iBAAkB,CAGd,qBAEA,qBAEA,qBAEA,8BAEJC,WAAY,CACRC,MAAO,MACPC,IAAK,OAETC,YAAa,eACbC,QAAS,CACLC,OAAQ,wBAEZC,SAAU,CACNC,KAAM,OACNC,QAAS,yBACTC,YAAa,gCACbC,iBAAkB,0BAClBC,QAAS"}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,70 @@
// 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 <http://www.gnu.org/licenses/>.
import {getButtonImage} from 'editor_tiny/utils';
import {getString} from 'core/str';
import {component, buttonName, icon} from 'tiny_equation/common';
import {handleAction} from 'tiny_equation/ui';
import {getSelectedEquation} from 'tiny_equation/equation';
import {isTexFilterActive} from 'tiny_equation/options';
/**
* Tiny Equation commands.
*
* @module tiny_equation/commands
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
export const getSetup = async() => {
const [
buttonText,
buttonImage,
] = await Promise.all([
getString('buttontitle', component),
getButtonImage('icon', component),
]);
return (editor) => {
if (isTexFilterActive(editor)) {
// Register the Equation Icon.
editor.ui.registry.addIcon(icon, buttonImage.html);
// Register the Menu Button as a toggle.
// This means that when highlighted over an existing Equation element it will show as toggled on.
editor.ui.registry.addToggleButton(buttonName, {
icon,
tooltip: buttonText,
onAction: () => {
handleAction(editor);
},
onSetup: (api) => {
editor.on('NodeChange', () => {
const result = getSelectedEquation(editor);
api.setActive(result);
});
},
});
// Add the Equation Menu Item.
// This allows it to be added to a standard menu, or a context menu.
editor.ui.registry.addMenuItem(buttonName, {
icon,
text: buttonText,
onAction: () => handleAction(editor),
});
}
};
};
@@ -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 <http://www.gnu.org/licenses/>.
/**
* Tiny Equation common values.
*
* @module tiny_equation/common
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
export default {
pluginName: 'tiny_equation/plugin',
component: 'tiny_equation',
buttonName: 'tiny_equation',
icon: 'tiny_equation',
};
@@ -0,0 +1,42 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Tiny Equation configuration.
*
* @module tiny_equation/configuration
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {component as buttonName} from 'tiny_equation/common';
import {
addMenubarItem,
addToolbarButton,
addToolbarSection,
} from 'editor_tiny/utils';
const configureToolbar = (toolbar) => {
addToolbarSection(toolbar, 'advanced', 'lists', true);
return addToolbarButton(toolbar, 'advanced', buttonName);
};
export const configure = (instanceConfig) => {
// Update the instance configuration to add the Equation menu option to the menus and toolbars.
return {
menu: addMenubarItem(instanceConfig.menu, 'insert', buttonName),
toolbar: configureToolbar(instanceConfig.toolbar),
};
};
@@ -0,0 +1,138 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Equation helper for Tiny Equation plugin.
*
* @module tiny_equation/equation
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Selectors from 'tiny_equation/selectors';
let sourceEquation = null;
/**
* Get the source equation.
* @returns {Object}
*/
export const getSourceEquation = () => sourceEquation;
/**
* Get selected equation.
* @param {TinyMCE} editor
* @returns {boolean}
*/
export const getSelectedEquation = (editor) => {
const currentSelection = editor.selection.getSel();
if (!currentSelection) {
// Do the early return if there is no text selected.
return false;
}
const textSelection = editor.selection.getNode().textContent;
const currentCaretPos = currentSelection.focusOffset;
let returnValue = false;
Selectors.equationPatterns.forEach((pattern) => {
// For each pattern in turn, find all whole matches (including the delimiters).
const regexPattern = new RegExp(pattern.source, "g");
[...textSelection.matchAll(regexPattern)].forEach((matches) => {
const match = matches[0];
// Check each occurrence of this match.
let startIndex = 0;
const startOuter = textSelection.indexOf(match, startIndex);
const endOuter = startOuter + match.length;
// This match is in our current position - fetch the innerMatch data.
const innerMatch = match.match(pattern);
if (innerMatch && innerMatch.length) {
// We need the start and end of the inner match for later.
const startInner = textSelection.indexOf(innerMatch[1], startOuter);
const endInner = startInner + innerMatch[1].length;
// We need to check the caret position before returning the match.
if (currentCaretPos >= startOuter && currentCaretPos <= endOuter) {
// We'll be returning the inner match for use in the editor itself.
returnValue = innerMatch[1];
// Save all data for later.
sourceEquation = {
// Inner match data.
startInnerPosition: startInner,
endInnerPosition: endInner,
innerMatch: innerMatch
};
return;
}
}
// Update the startIndex to match the end of the current match so that we can continue hunting
// for further matches.
startIndex = endOuter;
});
});
// We trim the equation when we load it and then add spaces when we save it.
if (returnValue !== false) {
returnValue = returnValue.trim();
} else {
// Clear the saved source equation.
sourceEquation = null;
}
return returnValue;
};
/**
* Get current equation data.
* @param {TinyMCE} editor
* @returns {{}}
*/
export const getCurrentEquationData = (editor) => {
let properties = {};
const equation = getSelectedEquation(editor);
if (equation) {
properties.equation = equation;
}
return properties;
};
/**
* Handle insertion of a new equation, or update of an existing one.
* @param {Element} currentForm
* @param {TinyMCE} editor
*/
export const setEquation = (currentForm, editor) => {
const input = currentForm.querySelector(Selectors.elements.equationTextArea);
const sourceEquation = getSourceEquation();
let value = input.value;
if (value !== '') {
if (sourceEquation) {
const selectedNode = editor.selection.getNode();
const text = selectedNode.textContent;
value = ' ' + value + ' ';
selectedNode.textContent = text.slice(0, sourceEquation.startInnerPosition)
+ value
+ text.slice(sourceEquation.endInnerPosition);
} else {
value = Selectors.delimiters.start + ' ' + value + ' ' + Selectors.delimiters.end;
editor.insertContent(value);
}
}
};
@@ -0,0 +1,46 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Equation Modal for Tiny.
*
* @module tiny_equation/modal
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Modal from 'core/modal';
export default class EquationModal extends Modal {
static TYPE = 'tiny_equation/modal';
static TEMPLATE = 'tiny_equation/modal';
registerEventListeners() {
// Call the parent registration.
super.registerEventListeners();
// Register to close on save/cancel.
this.registerCloseOnSave();
this.registerCloseOnCancel();
}
configure(modalConfig) {
modalConfig.show = true;
modalConfig.large = true;
modalConfig.removeOnClose = true;
super.configure(modalConfig);
}
}
@@ -0,0 +1,88 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Options helper for Tiny Equation plugin.
*
* @module tiny_equation/options
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {getPluginOptionName} from 'editor_tiny/options';
import {pluginName} from 'tiny_equation/common';
const librariesName = getPluginOptionName(pluginName, 'libraries');
const texFilterName = getPluginOptionName(pluginName, 'texfilter');
const contextIdName = getPluginOptionName(pluginName, 'contextid');
const texDocsUrlName = getPluginOptionName(pluginName, 'texdocsurl');
/**
* Register the options for the Tiny Equation plugin.
*
* @param {TinyMCE} editor
*/
export const register = (editor) => {
const registerOption = editor.options.register;
registerOption(librariesName, {
processor: 'array',
"default": [],
});
registerOption(texFilterName, {
processor: 'boolean',
"default": false,
});
registerOption(contextIdName, {
processor: 'number',
"default": 0,
});
registerOption(texDocsUrlName, {
processor: 'string',
"default": '',
});
};
/**
* Get the libraries configuration for the Tiny Equation plugin.
*
* @param {TinyMCE} editor
* @returns {object}
*/
export const getLibraries = (editor) => editor.options.get(librariesName);
/**
* Check if the TEX filter is active or not for the Tiny Equation plugin.
*
* @param {TinyMCE} editor
* @returns {boolean}
*/
export const isTexFilterActive = (editor) => editor.options.get(texFilterName);
/**
* Get the context id for the Tiny Equation plugin.
*
* @param {TinyMCE} editor
* @returns {number}
*/
export const getContextId = (editor) => editor.options.get(contextIdName);
/**
* Get the Tex Docs Url for the Tiny Equation plugin.
*
* @param {TinyMCE} editor
* @returns {string}
*/
export const getTexDocsUrl = (editor) => editor.options.get(texDocsUrlName);
@@ -0,0 +1,56 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Tiny Equation plugin for Moodle.
*
* @module tiny_equation/plugin
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @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 'tiny_equation/common';
import * as Commands from 'tiny_equation/commands';
import * as Configuration from 'tiny_equation/configuration';
import * as Options from 'tiny_equation/options';
// eslint-disable-next-line no-async-promise-executor
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) => {
// Register options.
Options.register(editor);
// Setup the Commands (buttons, menu items, and so on).
setupCommands(editor);
return pluginMetadata;
});
// Resolve the Equation Plugin and include configuration.
resolve([`${component}/plugin`, Configuration]);
});
@@ -0,0 +1,42 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* A javascript module to handle TinyMCE Equation ajax actions.
*
* @module tiny_equation/repository
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Ajax from 'core/ajax';
/**
* Filter the equation for given content.
*
* @param {Number} contextId The context id
* @param {String} content Content to filter
* @return {promise}
*/
export const filterEquation = (contextId, content) => {
const request = {
methodname: 'tiny_equation_filter',
args: {
contextid: contextId,
content: content,
}
};
return Ajax.call([request])[0];
};
@@ -0,0 +1,51 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Tiny Equation plugin helper function to build queryable data selectors.
*
* @module tiny_equation/selectors
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
export default {
equationPatterns: [
// We use space or not space because . does not match new lines.
// $$ blah $$.
/\$\$([\S\s]+?)\$\$/,
// E.g. "\( blah \)".
/\\\(([\S\s]+?)\\\)/,
// E.g. "\[ blah \]".
/\\\[([\S\s]+?)\\\]/,
// E.g. "[tex] blah [/tex]".
/\[tex\]([\S\s]+?)\[\/tex\]/
],
delimiters: {
start: '\\(',
end: '\\)'
},
cursorLatex: '\\Downarrow ',
actions: {
submit: '[data-action="save"]'
},
elements: {
form: 'form',
library: '.tiny_equation_library',
libraryItem: '.tiny_equation_library button',
equationTextArea: '.tiny_equation_equation',
preview: '.tiny_equation_preview',
}
};
@@ -0,0 +1,249 @@
// 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 <http://www.gnu.org/licenses/>.
/**
* Tiny Equation UI.
*
* @module tiny_equation/ui
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import EquationModal from 'tiny_equation/modal';
import ModalEvents from 'core/modal_events';
import {getContextId, getLibraries, getTexDocsUrl} from 'tiny_equation/options';
import {notifyFilterContentUpdated} from 'core/event';
import * as TinyEquationRepository from 'tiny_equation/repository';
import {exception as displayException} from 'core/notification';
import {debounce} from 'core/utils';
import Selectors from 'tiny_equation/selectors';
import {getSourceEquation, getCurrentEquationData, setEquation} from 'tiny_equation/equation';
let currentForm;
let lastCursorPos = 0;
/**
* Handle action
* @param {TinyMCE} editor
*/
export const handleAction = (editor) => {
displayDialogue(editor);
};
/**
* Display the equation editor
* @param {TinyMCE} editor
* @returns {Promise<void>}
*/
const displayDialogue = async(editor) => {
let data = {};
const currentEquationData = getCurrentEquationData(editor);
if (currentEquationData) {
Object.assign(data, currentEquationData);
}
const modal = await EquationModal.create({
templateContext: getTemplateContext(editor, data),
});
const $root = await modal.getRoot();
const root = $root[0];
currentForm = root.querySelector(Selectors.elements.form);
const contextId = getContextId(editor);
const debouncedPreviewUpdater = debounce(() => updatePreview(getContextId(editor)), 500);
$root.on(ModalEvents.shown, () => {
const library = root.querySelector(Selectors.elements.library);
TinyEquationRepository.filterEquation(contextId, library.innerHTML).then(async data => {
library.innerHTML = data.content;
updatePreview(contextId);
notifyFilter(library);
return data;
}).catch(displayException);
});
root.addEventListener('click', (e) => {
const libraryItem = e.target.closest(Selectors.elements.libraryItem);
const submitAction = e.target.closest(Selectors.actions.submit);
const textArea = e.target.closest('.tiny_equation_equation');
if (libraryItem) {
e.preventDefault();
selectLibraryItem(libraryItem, contextId);
}
if (submitAction) {
e.preventDefault();
setEquation(currentForm, editor);
modal.destroy();
}
if (textArea) {
debouncedPreviewUpdater();
}
});
root.addEventListener('keyup', (e) => {
const textArea = e.target.closest(Selectors.elements.equationTextArea);
if (textArea) {
debouncedPreviewUpdater();
}
});
root.addEventListener('keydown', (e) => {
const libraryItem = e.target.closest(Selectors.elements.libraryItem);
if (libraryItem) {
if (e.keyCode == 37 || e.keyCode == 39) {
groupNavigation(e);
}
}
});
};
/**
* Get template context.
* @param {TinyMCE} editor
* @param {Object} data
* @returns {Object}
*/
const getTemplateContext = (editor, data) => {
const libraries = getLibraries(editor);
const texDocsUrl = getTexDocsUrl(editor);
return Object.assign({}, {
elementid: editor.id,
elementidescaped: CSS.escape(editor.id),
libraries: libraries,
texdocsurl: texDocsUrl,
delimiters: Selectors.delimiters,
}, data);
};
/**
* Handle select library item.
* @param {Object} libraryItem
* @param {number} contextId
*/
const selectLibraryItem = (libraryItem, contextId) => {
const tex = libraryItem.getAttribute('data-tex');
const input = currentForm.querySelector(Selectors.elements.equationTextArea);
let oldValue;
let newValue;
let focusPoint = 0;
oldValue = input.value;
newValue = oldValue.substring(0, lastCursorPos);
if (newValue.charAt(newValue.length - 1) !== ' ') {
newValue += ' ';
}
newValue += tex;
focusPoint = newValue.length;
if (oldValue.charAt(lastCursorPos) !== ' ') {
newValue += ' ';
}
newValue += oldValue.substring(lastCursorPos, oldValue.length);
input.value = newValue;
input.focus();
input.selectionStart = input.selectionEnd = focusPoint;
updatePreview(contextId);
};
/**
* Update the preview section.
* @param {number} contextId
*/
const updatePreview = (contextId) => {
const textarea = currentForm.querySelector(Selectors.elements.equationTextArea);
const preview = currentForm.querySelector(Selectors.elements.preview);
const prefix = '';
const cursorLatex = Selectors.cursorLatex;
const isChar = /[a-zA-Z{]/;
let currentPos = textarea.selectionStart;
let equation = textarea.value;
// Move the cursor so it does not break expressions.
// Start at the very beginning.
if (!currentPos) {
currentPos = 0;
}
if (getSourceEquation()) {
currentPos = equation.length;
}
// First move back to the beginning of the line.
while (equation.charAt(currentPos) === '\\' && currentPos >= 0) {
currentPos -= 1;
}
if (currentPos !== 0) {
if (equation.charAt(currentPos - 1) != '{') {
// Now match to the end of the line.
while (isChar.test(equation.charAt(currentPos)) &&
currentPos < equation.length &&
isChar.test(equation.charAt(currentPos - 1))) {
currentPos += 1;
}
}
}
// Save the cursor position - for insertion from the library.
lastCursorPos = currentPos;
equation = prefix + equation.substring(0, currentPos) + cursorLatex + equation.substring(currentPos);
equation = Selectors.delimiters.start + ' ' + equation + ' ' + Selectors.delimiters.end;
TinyEquationRepository.filterEquation(contextId, equation).then((data) => {
preview.innerHTML = data.content;
notifyFilter(preview);
return data;
}).catch(displayException);
};
/**
* Notify the filters about the modified nodes
* @param {Element} element
*/
const notifyFilter = (element) => {
notifyFilterContentUpdated(element);
};
/**
* Callback handling the keyboard navigation in the groups of the library.
* @param {Event} e
*/
const groupNavigation = (e) => {
e.preventDefault();
const current = e.target.closest(Selectors.elements.libraryItem);
const parent = current.parentNode; // This must be the <div> containing all the buttons of the group.
const buttons = Array.prototype.slice.call(parent.querySelectorAll(Selectors.elements.libraryItem));
const direction = e.keyCode !== 37 ? 1 : -1;
let index = buttons.indexOf(current);
let nextButton;
if (index < 0) {
index = 0;
}
index += direction;
if (index < 0) {
index = buttons.length - 1;
} else if (index >= buttons.length) {
index = 0;
}
nextButton = buttons[index];
nextButton.focus();
};
@@ -0,0 +1,85 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
namespace tiny_equation\external;
use context;
use core_external\external_api;
use core_external\external_function_parameters;
use core_external\external_single_structure;
use core_external\external_value;
use filter_manager;
/**
* TinyMCE Equation external API for filtering the equation.
*
* @package tiny_equation
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class filter extends external_api {
/**
* Describes the parameters for filtering the equation.
*
* @return external_function_parameters
* @since Moodle 4.1
*/
public static function execute_parameters(): external_function_parameters {
return new external_function_parameters([
'contextid' => new external_value(PARAM_INT, 'The context ID', VALUE_REQUIRED),
'content' => new external_value(PARAM_RAW, 'The equation content', VALUE_REQUIRED)
]);
}
/**
* External function to filter the equation.
*
* @param int $contextid Context ID.
* @param string $content Equation content.
* @return array
* @since Moodle 4.1
*/
public static function execute(int $contextid, string $content): array {
[
'contextid' => $contextid,
'content' => $content
] = self::validate_parameters(self::execute_parameters(), [
'contextid' => $contextid,
'content' => $content
]);
$context = context::instance_by_id($contextid);
self::validate_context($context);
$result = filter_manager::instance()->filter_text($content, $context);
return [
'content' => $result,
];
}
/**
* Describes the data returned from the external function.
*
* @return external_single_structure
* @since Moodle 4.1
*/
public static function execute_returns(): external_single_structure {
return new external_single_structure([
'content' => new external_value(PARAM_RAW, 'Filtered content'),
]);
}
}
@@ -0,0 +1,106 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* Tiny equation plugin.
*
* @package tiny_equation
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tiny_equation;
use context;
use context_system;
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;
use filter_manager;
/**
* Tiny equation plugin.
*
* @package tiny_equation
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @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_equation/equation',
];
}
public static function get_available_menuitems(): array {
return [
'tiny_equation/equation',
];
}
public static function get_plugin_configuration_for_context(context $context, array $options, array $fpoptions,
?editor $editor = null): array {
$texexample = '$$\pi$$';
// Format a string with the active filter set.
// If it is modified - we assume that some sort of text filter is working in this context.
$formatoptions = [
'context' => $context,
];
$result = format_text($texexample, true, $formatoptions);
$texfilteractive = ($texexample !== $result);
if (isset($options['context'])) {
$context = $options['context'];
} else {
$context = context_system::instance();
}
$libraries = [
[
'key' => 'group1',
'groupname' => get_string('librarygroup1', 'tiny_equation'),
'elements' => explode("\n", trim(get_config('tiny_equation', 'librarygroup1'))),
'active' => true,
],
[
'key' => 'group2',
'groupname' => get_string('librarygroup2', 'tiny_equation'),
'elements' => explode("\n", trim(get_config('tiny_equation', 'librarygroup2'))),
],
[
'key' => 'group3',
'groupname' => get_string('librarygroup3', 'tiny_equation'),
'elements' => explode("\n", trim(get_config('tiny_equation', 'librarygroup3'))),
],
[
'key' => 'group4',
'groupname' => get_string('librarygroup4', 'tiny_equation'),
'elements' => explode("\n", trim(get_config('tiny_equation', 'librarygroup4'))),
]
];
return [
'texfilter' => $texfilteractive,
'contextid' => $context->id,
'libraries' => $libraries,
'texdocsurl' => get_docs_url('Using_TeX_Notation'),
];
}
}
@@ -0,0 +1,39 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* Privacy Subsystem implementation for the Equation plugin for TinyMCE.
*
* @package tiny_equation
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tiny_equation\privacy;
/**
* Privacy Subsystem implementation for the Equation plugin for TinyMCE.
*
* @package tiny_equation
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
public static function get_reason(): string {
return 'privacy:metadata';
}
}
@@ -0,0 +1,35 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* This file contains a controller for receiving Tiny Equation service requests
*
* @package tiny_equation
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
$functions = [
'tiny_equation_filter' => [
'classname' => 'tiny_equation\external\filter',
'methodname' => 'execute',
'description' => 'Filter the equation',
'type' => 'read',
'ajax' => true
],
];
@@ -0,0 +1,42 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* Strings for component 'tiny_equation', language 'en'.
*
* @package tiny_equation
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['buttontitle'] = 'Equation editor';
$string['cursorinfo'] = 'An arrow indicates the position that new elements from the element library will be inserted.';
$string['editequation'] = 'Edit equation using <a href="{$a}" target="_blank">TeX</a>';
$string['helplinktext'] = 'Equation helper';
$string['librarygroup1'] = 'Operators';
$string['librarygroup1_desc'] = 'TeX commands listed on the operators tab.';
$string['librarygroup2'] = 'Arrows';
$string['librarygroup2_desc'] = 'TeX commands listed on the arrows tab.';
$string['librarygroup3'] = 'Greek symbols';
$string['librarygroup3_desc'] = 'TeX commands listed on the Greek symbols tab.';
$string['librarygroup4'] = 'Advanced';
$string['librarygroup4_desc'] = 'TeX commands listed on the advanced tab.';
$string['modaltitle'] = 'Equation editor';
$string['pluginname'] = 'Equation editor';
$string['preview'] = 'Equation preview';
$string['privacy:metadata'] = 'The equation editor for TinyMCE does not store any personal data.';
$string['saveequation'] = 'Save equation';
$string['settings'] = 'Equation editor settings';
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 62 63.5"
style="enable-background:new 0 0 62 63.5;" xml:space="preserve" preserveAspectRatio="xMinYMid meet">
<g>
<path d="M57,56c0,2.3-1.9,4.2-4.2,4.2h-46c-2.3,0-4.2-1.9-4.2-4.2V5.6c0-2.3,1.9-4.2,4.2-4.2h46c2.3,0,4.2,1.9,4.2,4.2 V56z M52.8,7.7c0-1.2-0.9-2.1-2.1-2.1H8.9c-1.1,0-2.1,1-2.1,2.1v8.4c0,1.2,0.9,2.1,2.1,2.1h41.8c1.1,0,2.1-1,2.1-2.1V7.7z M11,22.4 c-2.3,0-4.2,1.9-4.2,4.2s1.9,4.2,4.2,4.2s4.2-1.9,4.2-4.2S13.3,22.4,11,22.4z M11,35c-2.3,0-4.2,1.9-4.2,4.2s1.9,4.2,4.2,4.2 s4.2-1.9,4.2-4.2S13.3,35,11,35z M11,47.6c-2.3,0-4.2,1.9-4.2,4.2S8.7,56,11,56s4.2-1.9,4.2-4.2S13.3,47.6,11,47.6z M23.5,22.4 c-2.3,0-4.2,1.9-4.2,4.2s1.9,4.2,4.2,4.2s4.2-1.9,4.2-4.2S25.9,22.4,23.5,22.4z M23.5,35c-2.3,0-4.2,1.9-4.2,4.2s1.9,4.2,4.2,4.2 s4.2-1.9,4.2-4.2S25.9,35,23.5,35z M23.5,47.6c-2.3,0-4.2,1.9-4.2,4.2s1.9,4.2,4.2,4.2s4.2-1.9,4.2-4.2S25.9,47.6,23.5,47.6z M36.1,22.4c-2.3,0-4.2,1.9-4.2,4.2s1.9,4.2,4.2,4.2s4.2-1.9,4.2-4.2S38.4,22.4,36.1,22.4z M36.1,35c-2.3,0-4.2,1.9-4.2,4.2 s1.9,4.2,4.2,4.2s4.2-1.9,4.2-4.2S38.4,35,36.1,35z M36.1,47.6c-2.3,0-4.2,1.9-4.2,4.2s1.9,4.2,4.2,4.2s4.2-1.9,4.2-4.2 S38.4,47.6,36.1,47.6z M48.6,22.4c-2.3,0-4.2,1.9-4.2,4.2s1.9,4.2,4.2,4.2s4.2-1.9,4.2-4.2S50.9,22.4,48.6,22.4z M52.8,39.2 c0-2.3-1.9-4.2-4.2-4.2s-4.2,1.9-4.2,4.2v12.6c0,2.3,1.9,4.2,4.2,4.2s4.2-1.9,4.2-4.2V39.2z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,174 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* Tiny equation plugin settings.
*
* @package tiny_equation
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die;
$ADMIN->add('editortiny', new admin_category('tiny_equation', new lang_string('pluginname', 'tiny_equation')));
$settings = new admin_settingpage('tiny_equation_settings', new lang_string('settings', 'tiny_equation'));
if ($ADMIN->fulltree) {
// Group 1.
$name = new lang_string('librarygroup1', 'tiny_equation');
$desc = new lang_string('librarygroup1_desc', 'tiny_equation');
$default = '
\cdot
\times
\ast
\div
\diamond
\pm
\mp
\oplus
\ominus
\otimes
\oslash
\odot
\circ
\bullet
\asymp
\equiv
\subseteq
\supseteq
\leq
\geq
\preceq
\succeq
\sim
\simeq
\approx
\subset
\supset
\ll
\gg
\prec
\succ
\infty
\in
\ni
\forall
\exists
\neq
';
$setting = new admin_setting_configtextarea('tiny_equation/librarygroup1',
$name,
$desc,
$default);
$settings->add($setting);
// Group 2.
$name = new lang_string('librarygroup2', 'tiny_equation');
$desc = new lang_string('librarygroup2_desc', 'tiny_equation');
$default = '
\leftarrow
\rightarrow
\uparrow
\downarrow
\leftrightarrow
\nearrow
\searrow
\swarrow
\nwarrow
\Leftarrow
\Rightarrow
\Uparrow
\Downarrow
\Leftrightarrow
';
$setting = new admin_setting_configtextarea('tiny_equation/librarygroup2',
$name,
$desc,
$default);
$settings->add($setting);
// Group 3.
$name = new lang_string('librarygroup3', 'tiny_equation');
$desc = new lang_string('librarygroup3_desc', 'tiny_equation');
$default = '
\alpha
\beta
\gamma
\delta
\epsilon
\zeta
\eta
\theta
\iota
\kappa
\lambda
\mu
\nu
\xi
\pi
\rho
\sigma
\tau
\upsilon
\phi
\chi
\psi
\omega
\Gamma
\Delta
\Theta
\Lambda
\Xi
\Pi
\Sigma
\Upsilon
\Phi
\Psi
\Omega
';
$setting = new admin_setting_configtextarea('tiny_equation/librarygroup3',
$name,
$desc,
$default);
$settings->add($setting);
// Group 4.
$name = new lang_string('librarygroup4', 'tiny_equation');
$desc = new lang_string('librarygroup4_desc', 'tiny_equation');
$default = '
\sum{a,b}
\sqrt[a]{b+c}
\int_{a}^{b}{c}
\iint_{a}^{b}{c}
\iiint_{a}^{b}{c}
\oint{a}
(a)
[a]
\lbrace{a}\rbrace
\left| \begin{matrix} a_1 & a_2 \\\\ a_3 & a_4 \end{matrix} \right|
\frac{a}{b+c}
\vec{a}
\binom {a} {b}
{a \brack b}
{a \brace b}
';
$setting = new admin_setting_configtextarea('tiny_equation/librarygroup4',
$name,
$desc,
$default);
$settings->add($setting);
}
@@ -0,0 +1,4 @@
.tiny_equation_library button {
margin: 0.25%;
min-width: 10%;
}
@@ -0,0 +1,68 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template tiny_equation/modal
Modal to manage an Equation within the Tiny Editor.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
Example context (json):
{
"elementid": "exampleId:1",
"elementidescaped": "exampleId\\:1",
"texdocsurl": "http://abc.com",
"equation": "\\Downarrow ",
"delimiters": {
"start": "",
"end": ""
},
"libraries": {
"group1": [
"\\Downarrow "
]
}
}
}}
{{< core/modal }}
{{$title}}
{{#str}} modaltitle, tiny_equation {{/str}}
{{/title}}
{{$body}}
<form class="mform" id="{{uniqid}}">
{{> tiny_equation/modal_library }}
<label for="{{elementid}}_tiny_equation_equation">{{#str}} editequation, tiny_equation, {{texdocsurl}} {{/str}}</label>
<textarea class="fullwidth text-ltr tiny_equation_equation w-100" id="{{elementid}}_tiny_equation_equation" rows="8">{{equation}}</textarea>
<label for="{{elementid}}_tiny_equation_preview">{{#str}} preview, tiny_equation {{/str}}</label>
<div aria-describedby="{{elementid}}_cursorinfo" class="border rounded bg-light p-1 fullwidth tiny_equation_preview" id="{{elementid}}__tiny_equation_preview"></div>
<div id="{{elementid}}_cursorinfo">{{#str}} cursorinfo, tiny_equation {{/str}}</div>
</form>
{{/body}}
{{$footer}}
<button type="button" class="btn btn-primary" data-action="save">{{#str}} saveequation, tiny_equation{{/str}}</button>
{{/footer}}
{{/ core/modal }}
@@ -0,0 +1,66 @@
{{!
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 <http://www.gnu.org/licenses/>.
}}
{{!
@template tiny_equation/modal_library
Equation library template.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
Example context (json):
{
"elementid": "exampleId:1",
"elementidescaped": "exampleId\\:1"
}
}}
<div class="tiny_equation_library">
<ul class="root nav nav-tabs mb-1" role="tablist">
{{#libraries}}
<li class="nav-item">
<a class="nav-link{{#active}} active{{/active}}"{{!
}}{{#active}} aria-selected="true"{{/active}}{{!
}}{{^active}} aria-selected="false" tabindex="-1"{{/active}}{{!
}} href="#{{elementid}}_tiny_equation_group_{{key}}"{{!
}} data-target="#{{elementidescaped}}_tiny_equation_group_{{key}}"
role="tab" data-toggle="tab"
>
{{groupname}}
</a>
</li>
{{/libraries}}
</ul>
<div class="tab-content mb-1 tiny_equation_groups">
{{#libraries}}
<div data-medium-type="link" class="tab-pane{{#active}} active{{/active}}"
id="{{elementid}}_tiny_equation_group_{{key}}">
<div role="toolbar">
{{#elements}}
<button class="btn btn-secondary tiny_equation_btn" data-tex="{{.}}" aria-label="{{.}}" title="{{.}}">
{{delimiters.start}} {{.}} {{delimiters.end}}
</button>
{{/elements}}
</div>
</div>
{{/libraries}}
</div>
</div>
@@ -0,0 +1,36 @@
@editor @editor_tiny @tiny_equation
Feature: 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 "<p>Equation test</p>"
# 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 "<p>\( \pi \)</p>"
# 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 "<p>\( \pi \)</p>"
@@ -0,0 +1,29 @@
<?php
// 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 <http://www.gnu.org/licenses/>.
/**
* Tiny equation plugin version details.
*
* @package tiny_equation
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2024042200;
$plugin->requires = 2024041600;
$plugin->component = 'tiny_equation';
+3
View File
@@ -0,0 +1,3 @@
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.getString)("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
@@ -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 <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny H5P Content configuration.\n *\n * @module tiny_h5p/commands\n * @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>\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 {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,kBAAU,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"}

Some files were not shown because too many files have changed in this diff Show More