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
+11
View File
@@ -0,0 +1,11 @@
define("editor_tiny/defaults",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getDefaultToolbar=_exports.getDefaultQuickbarsSelectionToolbar=_exports.getDefaultQuickbarsInsertToolbar=_exports.getDefaultQuickbarsImageToolbar=_exports.getDefaultMenu=_exports.getDefaultConfiguration=void 0;
/**
* TinyMCE Editor Upstream defaults.
*
* @module editor_tiny/defaults
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const getDefaultMenu=()=>({file:{title:"File",items:"newdocument restoredraft | preview | export print | deleteallconversations"},edit:{title:"Edit",items:"undo redo | cut copy paste pastetext | selectall | searchreplace"},view:{title:"View",items:"code | visualaid visualchars visualblocks | spellchecker | preview fullscreen | showcomments"},insert:{title:"Insert",items:"image link media addcomment pageembed template codesample inserttable | charmap emoticons hr | pagebreak nonbreaking anchor tableofcontents | insertdatetime"},format:{title:"Format",items:"bold italic underline strikethrough superscript subscript codeformat | styles blocks fontfamily fontsize align lineheight | forecolor backcolor | language | removeformat"},tools:{title:"Tools",items:"spellchecker spellcheckerlanguage | a11ycheck code wordcount"},table:{title:"Table",items:"inserttable | cell row column | advtablesort | tableprops deletetable"},help:{title:"Help",items:"help"}});_exports.getDefaultMenu=getDefaultMenu;const getDefaultToolbar=()=>[{name:"history",items:["undo","redo"]},{name:"formatting",items:["bold","italic"]},{name:"view",items:["fullscreen"]},{name:"alignment",items:["alignleft","aligncenter","alignright","alignjustify"]},{name:"indentation",items:["outdent","indent"]},{name:"lists",items:["bullist","numlist"]},{name:"comments",items:["addcomment"]}];_exports.getDefaultToolbar=getDefaultToolbar;const getDefaultQuickbarsSelectionToolbar=()=>"bold italic | quicklink h2 h3 blockquote";_exports.getDefaultQuickbarsSelectionToolbar=getDefaultQuickbarsSelectionToolbar;const getDefaultQuickbarsInsertToolbar=()=>"quickimage quicktable";_exports.getDefaultQuickbarsInsertToolbar=getDefaultQuickbarsInsertToolbar;const getDefaultQuickbarsImageToolbar=()=>"alignleft aligncenter alignright";_exports.getDefaultQuickbarsImageToolbar=getDefaultQuickbarsImageToolbar;_exports.getDefaultConfiguration=()=>({toolbar_mode:"sliding",toolbar:[{name:"history",items:["undo","redo"]},{name:"formatting",items:["bold","italic"]},{name:"view",items:["fullscreen"]},{name:"alignment",items:["alignleft","aligncenter","alignright","alignjustify"]},{name:"indentation",items:["outdent","indent"]},{name:"lists",items:["bullist","numlist"]},{name:"comments",items:["addcomment"]}],quickbars_selection_toolbar:"bold italic | quicklink h2 h3 blockquote",quickbars_insert_toolbar:"quickimage quicktable",quickbars_image_toolbar:"alignleft aligncenter alignright",menu:{file:{title:"File",items:"newdocument restoredraft | preview | export print | deleteallconversations"},edit:{title:"Edit",items:"undo redo | cut copy paste pastetext | selectall | searchreplace"},view:{title:"View",items:"code | visualaid visualchars visualblocks | spellchecker | preview fullscreen | showcomments"},insert:{title:"Insert",items:"image link media addcomment pageembed template codesample inserttable | charmap emoticons hr | pagebreak nonbreaking anchor tableofcontents | insertdatetime"},format:{title:"Format",items:"bold italic underline strikethrough superscript subscript codeformat | styles blocks fontfamily fontsize align lineheight | forecolor backcolor | language | removeformat"},tools:{title:"Tools",items:"spellchecker spellcheckerlanguage | a11ycheck code wordcount"},table:{title:"Table",items:"inserttable | cell row column | advtablesort | tableprops deletetable"},help:{title:"Help",items:"help"}},skin:"oxide"})}));
//# sourceMappingURL=defaults.min.js.map
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+11
View File
@@ -0,0 +1,11 @@
define("editor_tiny/loader",["exports","core/config"],(function(_exports,Config){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)}
/**
* Tiny Loader for Moodle
*
* @module editor_tiny/loader
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
let tinyMCEPromise;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getTinyMCE=_exports.baseUrl=void 0,Config=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}(Config);const baseUrl="".concat(Config.wwwroot,"/lib/editor/tiny/loader.php/").concat(M.cfg.jsrev);_exports.baseUrl=baseUrl;_exports.getTinyMCE=()=>tinyMCEPromise||(tinyMCEPromise=new Promise(((resolve,reject)=>{const head=document.querySelector("head");let script=head.querySelector('script[data-tinymce="tinymce"]');script&&resolve(window.tinyMCE),script=document.createElement("script"),script.dataset.tinymce="tinymce",script.src="".concat(baseUrl,"/tinymce.js"),script.async=!0,script.addEventListener("load",(()=>{resolve(window.tinyMCE)}),!1),script.addEventListener("error",(err=>{reject(err)}),!1),head.append(script)})),tinyMCEPromise)}));
//# sourceMappingURL=loader.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"loader.min.js","sources":["../src/loader.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 Loader for Moodle\n *\n * @module editor_tiny/loader\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\nlet tinyMCEPromise;\n\nimport * as Config from 'core/config';\n\nexport const baseUrl = `${Config.wwwroot}/lib/editor/tiny/loader.php/${M.cfg.jsrev}`;\n\n/**\n * Get the TinyMCE API Object.\n *\n * @returns {Promise<TinyMCE>} The TinyMCE API Object\n */\nexport const getTinyMCE = () => {\n if (tinyMCEPromise) {\n return tinyMCEPromise;\n }\n\n tinyMCEPromise = new Promise((resolve, reject) => {\n const head = document.querySelector('head');\n let script = head.querySelector('script[data-tinymce=\"tinymce\"]');\n if (script) {\n resolve(window.tinyMCE);\n }\n\n script = document.createElement('script');\n script.dataset.tinymce = 'tinymce';\n script.src = `${baseUrl}/tinymce.js`;\n script.async = true;\n\n script.addEventListener('load', () => {\n resolve(window.tinyMCE);\n }, false);\n\n script.addEventListener('error', (err) => {\n reject(err);\n }, false);\n\n head.append(script);\n });\n\n return tinyMCEPromise;\n};\n"],"names":["tinyMCEPromise","baseUrl","Config","wwwroot","M","cfg","jsrev","Promise","resolve","reject","head","document","querySelector","script","window","tinyMCE","createElement","dataset","tinymce","src","async","addEventListener","err","append"],"mappings":";;;;;;;;IAuBIA,qxBAISC,kBAAaC,OAAOC,+CAAsCC,EAAEC,IAAIC,oDAOnD,IAClBN,iBAIJA,eAAiB,IAAIO,SAAQ,CAACC,QAASC,gBAC7BC,KAAOC,SAASC,cAAc,YAChCC,OAASH,KAAKE,cAAc,kCAC5BC,QACAL,QAAQM,OAAOC,SAGnBF,OAASF,SAASK,cAAc,UAChCH,OAAOI,QAAQC,QAAU,UACzBL,OAAOM,cAASlB,uBAChBY,OAAOO,OAAQ,EAEfP,OAAOQ,iBAAiB,QAAQ,KAC5Bb,QAAQM,OAAOC,YAChB,GAEHF,OAAOQ,iBAAiB,SAAUC,MAC9Bb,OAAOa,QACR,GAEHZ,KAAKa,OAAOV,WAGTb"}
+3
View File
@@ -0,0 +1,3 @@
define("editor_tiny/options",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.registerPlaceholderSelectors=_exports.register=_exports.getPluginOptionName=_exports.getPlaceholderSelectors=_exports.getMoodleLang=_exports.getInitialPluginConfiguration=_exports.getFilepickers=_exports.getFilePicker=_exports.getDraftItemId=_exports.getCurrentLanguage=_exports.getContextId=void 0;_exports.register=(editor,options)=>{const registerOption=editor.options.register,setOption=editor.options.set;registerOption("moodle:contextid",{processor:"number",default:0}),setOption("moodle:contextid",options.context),registerOption("moodle:filepickers",{processor:"object",default:{}}),setOption("moodle:filepickers",Object.assign({},options.filepicker)),registerOption("moodle:draftitemid",{processor:"number",default:0}),setOption("moodle:draftitemid",options.draftitemid),registerOption("moodle:currentLanguage",{processor:"string",default:"en"}),setOption("moodle:currentLanguage",options.currentLanguage),registerOption("moodle:language",{processor:"object",default:{}}),setOption("moodle:language",options.language),registerOption("moodle:placeholderSelectors",{processor:"array",default:[]}),setOption("moodle:placeholderSelectors",options.placeholderSelectors)};_exports.getContextId=editor=>editor.options.get("moodle:contextid");_exports.getDraftItemId=editor=>editor.options.get("moodle:draftitemid");const getFilepickers=editor=>editor.options.get("moodle:filepickers");_exports.getFilepickers=getFilepickers;_exports.getFilePicker=(editor,type)=>getFilepickers(editor)[type];_exports.getMoodleLang=editor=>editor.options.get("moodle:language");_exports.getCurrentLanguage=editor=>editor.options.get("moodle:currentLanguage");_exports.getInitialPluginConfiguration=options=>{const config={};return Object.entries(options.plugins).forEach((_ref=>{var _pluginConfig$config;let[pluginName,pluginConfig]=_ref;Object.entries(null!==(_pluginConfig$config=pluginConfig.config)&&void 0!==_pluginConfig$config?_pluginConfig$config:{}).forEach((_ref2=>{let[optionName,value]=_ref2;config[getPluginOptionName(pluginName,optionName)]=value}))})),config};const getPluginOptionName=(pluginName,optionName)=>"".concat(pluginName,":").concat(optionName);_exports.getPluginOptionName=getPluginOptionName;const getPlaceholderSelectors=editor=>editor.options.get("moodle:placeholderSelectors");_exports.getPlaceholderSelectors=getPlaceholderSelectors;_exports.registerPlaceholderSelectors=(editor,selectors)=>{if(selectors.length){let existingData=getPlaceholderSelectors(editor);existingData=existingData.concat(selectors),editor.options.set("moodle:placeholderSelectors",existingData)}}}));
//# sourceMappingURL=options.min.js.map
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
define("editor_tiny/uploader",["exports","core_form/events","editor_tiny/options"],(function(_exports,_events,_options){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default=(editor,filePickerType,blob,fileName,progress)=>new Promise(((resolve,reject)=>{var _options$savepath;(0,_events.notifyUploadStarted)(editor.targetElm.id);const xhr=new XMLHttpRequest;xhr.upload.addEventListener("progress",(e=>{progress(e.loaded/e.total*100)})),xhr.addEventListener("load",(()=>{if(403===xhr.status)return void reject({message:"HTTP error: ".concat(xhr.status),remove:!0});if(xhr.status<200||xhr.status>=300)return void reject("HTTP Error: ".concat(xhr.status));const response=JSON.parse(xhr.responseText);if(!response)return void reject("Invalid JSON: ".concat(xhr.responseText));let location;if((0,_events.notifyUploadCompleted)(editor.targetElm.id),response.url?location=response.url:response.event&&"fileexists"===response.event&&response.newfile&&(location=response.newfile.url),location&&"string"==typeof location)return void resolve(location);const errorString=xhr.responseText;let output="";try{output=JSON.parse(errorString)}catch(error){output=errorString}reject(output)})),xhr.addEventListener("error",(()=>{reject({message:"Upload failed due to an XHR transport error. Code: ".concat(xhr.status),remove:!0})}));const formData=new FormData,options=(0,_options.getFilePicker)(editor,filePickerType);formData.append("repo_upload_file",blob,fileName),formData.append("itemid",options.itemid),Object.values(options.repositories).some((repository=>"upload"===repository.type&&(formData.append("repo_id",repository.id),!0))),formData.append("env",options.env),formData.append("sesskey",M.cfg.sesskey),formData.append("client_id",options.client_id),formData.append("savepath",null!==(_options$savepath=options.savepath)&&void 0!==_options$savepath?_options$savepath:"/"),formData.append("ctx_id",options.context.id);const acceptedTypes=options.accepted_types;Array.isArray(acceptedTypes)?acceptedTypes.forEach((function(type){formData.append("accepted_types[]",type)})):formData.append("accepted_types[]",acceptedTypes),xhr.open("POST","".concat(M.cfg.wwwroot,"/repository/repository_ajax.php?action=upload"),!0),xhr.send(formData)})),_exports.default}));
//# sourceMappingURL=uploader.min.js.map
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+179
View File
@@ -0,0 +1,179 @@
// 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/>.
/* eslint-disable max-len, */
/**
* TinyMCE Editor Upstream defaults.
*
* @module editor_tiny/defaults
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* The upstream defaults for the TinyMCE Menu.
*
* This value is defined in the TinyMCE documentation, but not exported anywhere useful.
* https://www.tiny.cloud/docs/tinymce/6/menus-configuration-options/#menu
*
* @returns {Object}
*/
export const getDefaultMenu = () => {
return {
file: {title: 'File', items: 'newdocument restoredraft | preview | export print | deleteallconversations'},
edit: {title: 'Edit', items: 'undo redo | cut copy paste pastetext | selectall | searchreplace'},
view: {title: 'View', items: 'code | visualaid visualchars visualblocks | spellchecker | preview fullscreen | showcomments'},
insert: {title: 'Insert', items: 'image link media addcomment pageembed template codesample inserttable | charmap emoticons hr | pagebreak nonbreaking anchor tableofcontents | insertdatetime'},
format: {title: 'Format', items: 'bold italic underline strikethrough superscript subscript codeformat | styles blocks fontfamily fontsize align lineheight | forecolor backcolor | language | removeformat'},
tools: {title: 'Tools', items: 'spellchecker spellcheckerlanguage | a11ycheck code wordcount'},
table: {title: 'Table', items: 'inserttable | cell row column | advtablesort | tableprops deletetable'},
help: {title: 'Help', items: 'help'}
};
};
/**
* The default toolbar configuration to use.
*
* This is based upon the default value used if no toolbar is specified.
*
* https://www.tiny.cloud/docs/tinymce/6/menus-configuration-options/#menu
*
* @returns {Object}
*/
export const getDefaultToolbar = () => {
return [
{
name: 'history',
items: [
'undo',
'redo',
],
},
{
name: 'formatting',
items: [
'bold',
'italic',
],
},
{
name: 'view',
items: ['fullscreen'],
},
{
name: 'alignment',
items: [
'alignleft',
'aligncenter',
'alignright',
'alignjustify',
],
},
{
name: 'indentation',
items: [
'outdent',
'indent',
],
},
{
name: 'lists',
items: [
'bullist',
'numlist',
],
},
{
name: 'comments',
items: ['addcomment'],
},
];
};
/**
* The default quickbars_insert_toolbar configuration to use.
*
* This is based upon the default value used if no toolbar is specified.
*
* https://www.tiny.cloud/docs/tinymce/6/quickbars/#quickbars_selection_toolbar
*
* @returns {string}
*/
export const getDefaultQuickbarsSelectionToolbar = () => 'bold italic | quicklink h2 h3 blockquote';
/**
* The default quickbars_insert_toolbar configuration to use.
*
* This is based upon the default value used if no toolbar is specified.
*
* https://www.tiny.cloud/docs/tinymce/6/quickbars/#quickbars_insert_toolbar
*
* @returns {string}
*/
export const getDefaultQuickbarsInsertToolbar = () => 'quickimage quicktable';
/**
* The default quickbars_insert_toolbar configuration to use.
*
* This is based upon the default value used if no toolbar is specified.
*
* https://www.tiny.cloud/docs/tinymce/6/quickbars/#quickbars_image_toolbar
*
* @returns {string}
*/
export const getDefaultQuickbarsImageToolbar = () => 'alignleft aligncenter alignright';
/**
* Get the default configuration provided by TinyMCE.
*
* @returns {object}
*/
export const getDefaultConfiguration = () => ({
// Toolbar configuration.
// https://www.tiny.cloud/docs/tinymce/6/toolbar-configuration-options/
// TODO: Move this configuration to a passed-in option.
// eslint-disable-next-line camelcase
toolbar_mode: 'sliding',
toolbar: getDefaultToolbar(),
// Quickbars Selection Toolbar configuration.
// https://www.tiny.cloud/docs/tinymce/6/quickbars/#quickbars_selection_toolbar
// eslint-disable-next-line camelcase
quickbars_selection_toolbar: getDefaultQuickbarsSelectionToolbar(),
// Quickbars Select Toolbar configuration.
// https://www.tiny.cloud/docs/tinymce/6/quickbars/#quickbars_insert_toolbar
// eslint-disable-next-line camelcase
quickbars_insert_toolbar: getDefaultQuickbarsInsertToolbar(),
// Quickbars Image Toolbar configuration.
// https://www.tiny.cloud/docs/tinymce/6/quickbars/#quickbars_image_toolbar
// eslint-disable-next-line camelcase
quickbars_image_toolbar: getDefaultQuickbarsImageToolbar(),
// Menu configuration.
// https://www.tiny.cloud/docs/tinymce/6/menus-configuration-options/
// TODO: Move this configuration to a passed-in option.
menu: getDefaultMenu(),
// Mobile configuration.
// At this time we will use the default TinyMCE mobile configuration.
// https://www.tiny.cloud/docs/tinymce/6/tinymce-for-mobile/
// Skins
skin: 'oxide',
});
+566
View File
@@ -0,0 +1,566 @@
// 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/>.
/**
* TinyMCE Editor Manager.
*
* @module editor_tiny/editor
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import jQuery from 'jquery';
import Pending from 'core/pending';
import {getDefaultConfiguration, getDefaultQuickbarsSelectionToolbar} from './defaults';
import {getTinyMCE, baseUrl} from './loader';
import * as Options from './options';
import {addToolbarButton, addToolbarButtons, addToolbarSection,
removeToolbarButton, removeSubmenuItem, updateEditorState} from './utils';
/**
* Storage for the TinyMCE instances on the page.
* @type {Map}
*/
const instanceMap = new Map();
/**
* The default editor configuration.
* @type {Object}
*/
let defaultOptions = {};
/**
* Require the modules for the named set of TinyMCE plugins.
*
* @param {string[]} pluginList The list of plugins
* @return {Promise[]} A matching set of Promises relating to the requested plugins
*/
const importPluginList = async(pluginList) => {
// Fetch all of the plugins from the list of plugins.
// If a plugin contains a '/' then it is assumed to be a Moodle AMD module to import.
const pluginHandlers = await Promise.all(pluginList.map(pluginPath => {
if (pluginPath.indexOf('/') === -1) {
// A standard TinyMCE Plugin.
return Promise.resolve(pluginPath);
}
return import(pluginPath);
}));
// Normalise the plugin data to a list of plugin names.
// Two formats are supported:
// - a string; and
// - an array whose first element is the plugin name, and the second element is the plugin configuration.
const pluginNames = pluginHandlers.map((pluginConfig) => {
if (typeof pluginConfig === 'string') {
return pluginConfig;
}
if (Array.isArray(pluginConfig)) {
return pluginConfig[0];
}
return null;
}).filter((value) => value);
// Fetch the list of pluginConfig handlers.
const pluginConfig = pluginHandlers.map((pluginConfig) => {
if (Array.isArray(pluginConfig)) {
return pluginConfig[1];
}
return null;
}).filter((value) => value);
return {
pluginNames,
pluginConfig,
};
};
/**
* Fetch the language data for the specified language.
*
* @param {string} language The language identifier
* @returns {object}
*/
const fetchLanguage = (language) => fetch(
`${M.cfg.wwwroot}/lib/editor/tiny/lang.php/${M.cfg.langrev}/${language}`
).then(response => response.json());
/**
* Get a list of all Editors in a Map, keyed by the DOM Node that the Editor is associated with.
*
* @returns {Map<Node, Editor>}
*/
export const getAllInstances = () => new Map(instanceMap.entries());
/**
* Get the TinyMCE instance for the specified Node ID.
*
* @param {string} elementId
* @returns {TinyMCE|undefined}
*/
export const getInstanceForElementId = elementId => getInstanceForElement(document.getElementById(elementId));
/*
* Get the TinyMCE instance for the specified HTMLElement.
*
* @param {HTMLElement} element
* @returns {TinyMCE|undefined}
*/
export const getInstanceForElement = element => {
const instance = instanceMap.get(element);
if (instance && instance.removed) {
instanceMap.delete(element);
return undefined;
}
return instance;
};
/**
* Set up TinyMCE for the selector at the specified HTML Node id.
*
* @param {object} config The configuration required to setup the editor
* @param {string} config.elementId The HTML Node ID
* @param {Object} config.options The editor plugin configuration
*/
export const setupForElementId = ({elementId, options}) => {
const target = document.getElementById(elementId);
// We will need to wrap the setupForTarget and editor.remove() calls in a setTimeout.
// Because other events callbacks will still try to run on the removed instance.
// This will cause an error on Firefox.
// We need to make TinyMCE to remove itself outside the event loop.
// @see https://github.com/tinymce/tinymce/issues/3129 for more details.
setTimeout(() => {
return setupForTarget(target, options);
}, 1);
};
/**
* Initialise the page with standard TinyMCE requirements.
*
* Currently this includes the language taken from the HTML lang property.
*/
const initialisePage = async() => {
const lang = document.querySelector('html').lang;
const [tinyMCE, langData] = await Promise.all([getTinyMCE(), fetchLanguage(lang)]);
tinyMCE.addI18n(lang, langData);
};
initialisePage();
/**
* Get the list of plugins to load for the specified configuration.
*
* If the specified configuration does not include a plugin configuration, then return the default configuration.
*
* @param {object} options
* @param {array} [options.plugins=null] The plugin list
* @returns {object}
*/
const getPlugins = ({plugins = null} = {}) => {
if (plugins) {
return plugins;
}
if (defaultOptions.plugins) {
return defaultOptions.plugins;
}
return {};
};
/**
* Adjust the editor size base on the target element.
*
* @param {TinyMCE} editor TinyMCE editor
* @param {Node} target Target element
*/
const adjustEditorSize = (editor, target) => {
let expectedEditingAreaHeight = 0;
if (target.clientHeight) {
expectedEditingAreaHeight = target.clientHeight;
} else {
// If the target element is hidden, we cannot get the lineHeight of the target element.
// We don't have a proper way to retrieve the general lineHeight of the theme, so we use 22 here, it's equivalent to 1.5em.
expectedEditingAreaHeight = target.rows * (parseFloat(window.getComputedStyle(target).lineHeight) || 22);
}
const currentEditingAreaHeight = editor.getContainer().querySelector('.tox-sidebar-wrap').clientHeight;
if (currentEditingAreaHeight < expectedEditingAreaHeight) {
// Change the height based on the target element's height.
editor.getContainer().querySelector('.tox-sidebar-wrap').style.height = `${expectedEditingAreaHeight}px`;
}
};
/**
* Get the standard configuration for the specified options.
*
* @param {Node} target
* @param {tinyMCE} tinyMCE
* @param {object} options
* @param {Array} plugins
* @returns {object}
*/
const getStandardConfig = (target, tinyMCE, options, plugins) => {
const lang = document.querySelector('html').lang;
const config = Object.assign({}, getDefaultConfiguration(), {
// eslint-disable-next-line camelcase
base_url: baseUrl,
// Set the editor target.
// https://www.tiny.cloud/docs/tinymce/6/editor-important-options/#target
target,
// https://www.tiny.cloud/docs/tinymce/6/customize-ui/#set-maximum-and-minimum-heights-and-widths
// Set the minimum height to the smallest height that we can fit the Menu bar, Tool bar, Status bar and the text area.
// eslint-disable-next-line camelcase
min_height: 175,
// Base the height on the size of the text area.
// In some cases, E.g.: The target is an advanced element, it will be hidden. We cannot get the height at this time.
// So set the height to auto, and adjust it later by adjustEditorSize().
height: target.clientHeight || 'auto',
// Set the language.
// https://www.tiny.cloud/docs/tinymce/6/ui-localization/#language
// eslint-disable-next-line camelcase
language: lang,
// Load the editor stylesheet into the editor iframe.
// https://www.tiny.cloud/docs/tinymce/6/add-css-options/
// eslint-disable-next-line camelcase
content_css: [
options.css,
],
// Do not convert URLs to relative URLs.
// https://www.tiny.cloud/docs/tinymce/6/url-handling/#convert_urls
// eslint-disable-next-line camelcase
convert_urls: false,
// Enabled 'advanced' a11y options.
// This includes allowing role="presentation" from the image uploader.
// https://www.tiny.cloud/docs/tinymce/6/accessibility/
// eslint-disable-next-line camelcase
a11y_advanced_options: true,
// Add specific rules to the valid elements.
// eslint-disable-next-line camelcase
extended_valid_elements: 'script[*],p[*],i[*]',
// Disable XSS Sanitisation.
// We do this in PHP.
// https://www.tiny.cloud/docs/tinymce/6/security/#turning-dompurify-off
// Note: This feature has been backported from TinyMCE 6.4.0.
// eslint-disable-next-line camelcase
xss_sanitization: false,
// Disable quickbars entirely.
// The UI is not ideal and we'll wait for it to improve in future before we enable it in Moodle.
// eslint-disable-next-line camelcase
quickbars_insert_toolbar: '',
// If the target element is too small, disable the quickbars selection toolbar.
// The quickbars selection toolbar is not displayed correctly if the target element is too small.
// See: https://github.com/tinymce/tinymce/issues/9693.
quickbars_selection_toolbar: target.rows > 5 ? getDefaultQuickbarsSelectionToolbar() : false,
// Override the standard block formats property (removing h1 & h2).
// https://www.tiny.cloud/docs/tinymce/6/user-formatting-options/#block_formats
// eslint-disable-next-line camelcase
block_formats: 'Paragraph=p;Heading 3=h3;Heading 4=h4;Heading 5=h5;Heading 6=h6;Preformatted=pre',
// The list of plugins to include in the instance.
// https://www.tiny.cloud/docs/tinymce/6/editor-important-options/#plugins
plugins: [
...plugins,
],
// Skins
skin: 'oxide',
// Do not show the help link in the status bar.
// https://www.tiny.cloud/docs/tinymce/latest/accessibility/#help_accessibility
// eslint-disable-next-line camelcase
help_accessibility: false,
// Remove the "Upgrade" link for Tiny.
// https://www.tiny.cloud/docs/tinymce/6/editor-premium-upgrade-promotion/
promotion: false,
// Allow the administrator to disable branding.
// https://www.tiny.cloud/docs/tinymce/6/statusbar-configuration-options/#branding
branding: options.branding,
// Put th cells in a thead element.
// https://www.tiny.cloud/docs/tinymce/6/table-options/#table_header_type
// eslint-disable-next-line camelcase
table_header_type: 'sectionCells',
// Stored text in non-entity form.
// https://www.tiny.cloud/docs/tinymce/6/content-filtering/#entity_encoding
// eslint-disable-next-line camelcase
entity_encoding: "raw",
// Enable support for editors in scrollable containers.
// https://www.tiny.cloud/docs/tinymce/6/ui-mode-configuration-options/#ui_mode
// eslint-disable-next-line camelcase
ui_mode: 'split',
// Enable browser-supported spell checking.
// https://www.tiny.cloud/docs/tinymce/latest/spelling/
// eslint-disable-next-line camelcase
browser_spellcheck: true,
setup: (editor) => {
Options.register(editor, options);
editor.on('PreInit', function() {
// Work around a bug in TinyMCE with Firefox.
// When an editor is removed, and replaced with an identically attributed editor (same ID),
// and the Firefox window is freshly opened (e.g. Behat, Private browsing), the wrong contentWindow
// is assigned to the editor instance leading to an NS_ERROR_UNEXPECTED error in Firefox.
// This is a workaround for that issue.
this.contentWindow = this.iframeElement.contentWindow;
});
editor.on('init', function() {
// Hide justify alignment sub-menu.
removeSubmenuItem(editor, 'align', 'tiny:justify');
// Adjust the editor size.
adjustEditorSize(editor, target);
});
target.addEventListener('form:editorUpdated', function() {
updateEditorState(editor, target);
});
target.dispatchEvent(new Event('form:editorUpdated'));
},
});
config.toolbar = addToolbarSection(config.toolbar, 'content', 'formatting', true);
config.toolbar = addToolbarButton(config.toolbar, 'content', 'link');
// Add directionality plugins, always.
config.toolbar = addToolbarSection(config.toolbar, 'directionality', 'alignment', true);
config.toolbar = addToolbarButtons(config.toolbar, 'directionality', ['ltr', 'rtl']);
// Remove the align justify button from the toolbar.
config.toolbar = removeToolbarButton(config.toolbar, 'alignment', 'alignjustify');
return config;
};
/**
* Fetch the TinyMCE configuration for this editor instance.
*
* @param {HTMLElement} target
* @param {TinyMCE} tinyMCE The TinyMCE API
* @param {Object} options The editor plugin configuration
* @param {object} pluginValues
* @param {object} pluginValues.pluginConfig The list of plugin configuration
* @param {object} pluginValues.pluginNames The list of plugins to load
* @returns {object} The TinyMCE Configuration
*/
const getEditorConfiguration = (target, tinyMCE, options, pluginValues) => {
const {
pluginNames,
pluginConfig,
} = pluginValues;
// Allow plugins to modify the configuration.
// This seems a little strange, but we must double-process the config slightly.
// First we fetch the standard configuration.
const instanceConfig = getStandardConfig(target, tinyMCE, options, pluginNames);
// Next we make any standard changes.
// Here we remove the file menu, as it doesn't offer any useful functionality.
// We only empty the items list so that a plugin may choose to add to it themselves later if they wish.
if (instanceConfig.menu.file) {
instanceConfig.menu.file.items = '';
}
// We disable the styles, backcolor, and forecolor plugins from the format menu.
// These are not useful for Moodle and we don't want to encourage their use.
if (instanceConfig.menu.format) {
instanceConfig.menu.format.items = instanceConfig.menu.format.items
// Remove forecolor and backcolor.
.replace(/forecolor ?/, '')
.replace(/backcolor ?/, '')
// Remove fontfamily for now.
.replace(/fontfamily ?/, '')
// Remove fontsize for now.
.replace(/fontsize ?/, '')
// Remove styles - it just duplicates the format menu in a way which does not respect configuration
.replace(/styles ?/, '')
// Remove any duplicate separators.
.replaceAll(/\| *\|/g, '|');
}
if (instanceConfig.quickbars_selection_toolbar !== false) {
// eslint-disable-next-line camelcase
instanceConfig.quickbars_selection_toolbar = instanceConfig.quickbars_selection_toolbar.replace('h2 h3', 'h3 h4 h5 h6');
}
// Next we call the `configure` function for any plugin which defines it.
// We pass the current instanceConfig in here, to allow them to make certain changes to the global configuration.
// For example, to add themselves to any menu, toolbar, and so on.
// Any plugin which wishes to have configuration options must register those options here.
pluginConfig.filter((pluginConfig) => typeof pluginConfig.configure === 'function').forEach((pluginConfig) => {
const pluginInstanceOverride = pluginConfig.configure(instanceConfig, options);
Object.assign(instanceConfig, pluginInstanceOverride);
});
// Next we convert the plugin configuration into a format that TinyMCE understands.
Object.assign(instanceConfig, Options.getInitialPluginConfiguration(options));
return instanceConfig;
};
/**
* Check if the target for TinyMCE is in a modal or not.
*
* @param {HTMLElement} target Target to check
* @returns {boolean} True if the target is in a modal form.
*/
const isModalMode = (target) => {
return !!target.closest('[data-region="modal"]');
};
/**
* Set up TinyMCE for the HTML Element.
*
* @param {HTMLElement} target
* @param {Object} [options={}] The editor plugin configuration
* @return {Promise<TinyMCE>} The TinyMCE instance
*/
export const setupForTarget = async(target, options = {}) => {
const instance = getInstanceForElement(target);
if (instance) {
return Promise.resolve(instance);
}
// Register a new pending promise to ensure that Behat waits for the editor setup to complete before continuing.
const pendingPromise = new Pending('editor_tiny/editor:setupForTarget');
// Get the list of plugins.
const plugins = getPlugins(options);
// Fetch the tinyMCE API, and instantiate the plugins.
const [tinyMCE, pluginValues] = await Promise.all([
getTinyMCE(),
importPluginList(Object.keys(plugins)),
]);
// TinyMCE uses the element ID as a map key internally, even if the target has changed.
// In the case where we have an editor in a modal form which has been detached from the DOM, but the editor not removed,
// we need to manually destroy the editor.
// We could theoretically do this with a Mutation Observer, but in some cases the Node may be moved,
// or added back elsewhere in the DOM.
// First remove any detached editors.
tinyMCE.get().filter((editor) => !editor.getElement().isConnected).forEach((editor) => {
editor.remove();
});
// Now check for any existing editor which shares the same ID.
const existingEditor = tinyMCE.EditorManager.get(target.id);
if (existingEditor) {
if (existingEditor.getElement() === target) {
pendingPromise.resolve();
return Promise.resolve(existingEditor);
} else {
pendingPromise.resolve();
throw new Error('TinyMCE instance already exists for different target with same ID');
}
}
// Get the editor configuration for this editor.
const instanceConfig = getEditorConfiguration(target, tinyMCE, options, pluginValues);
// Initialise the editor instance for the given configuration.
// At this point any plugin which has configuration options registered will have them applied for this instance.
const [editor] = await tinyMCE.init(instanceConfig);
// Update the textarea when the editor to set the field type for Behat.
target.dataset.fieldtype = 'editor';
// Store the editor instance in the instanceMap and register a listener on removal to remove it from the map.
instanceMap.set(target, editor);
editor.on('remove', ({target}) => {
// Handle removal of the editor from the map on destruction.
instanceMap.delete(target.targetElm);
target.targetElm.dataset.fieldtype = null;
});
// If the editor is part of a form, also listen to the jQuery submit event.
// The jQuery submit event will not trigger the native submit event, and therefore the content will not be saved.
// We cannot rely on listening to the bubbled submit event on the document because other events on child nodes may
// consume the data before it is saved.
if (target.form) {
jQuery(target.form).on('submit', () => {
editor.save();
});
}
// Save the editor content to the textarea when the editor is blurred.
editor.on('blur', () => {
editor.save();
});
// If the editor is in a modal, we need to hide the modal when window editor's window is opened.
editor.on('OpenWindow', () => {
const modals = document.querySelectorAll('[data-region="modal"]');
if (modals) {
modals.forEach((modal) => {
if (!modal.classList.contains('hide')) {
modal.classList.add('hide');
}
});
}
});
// If the editor's window is closed, we need to show the hidden modal back.
editor.on('CloseWindow', () => {
if (isModalMode(target)) {
const modals = document.querySelectorAll('[data-region="modal"]');
if (modals) {
modals.forEach((modal) => {
if (modal.classList.contains('hide')) {
modal.classList.remove('hide');
}
});
}
}
});
pendingPromise.resolve();
return editor;
};
/**
* Set the default editor configuration.
*
* This configuration is used when an editor is initialised without any configuration.
*
* @param {object} [options={}]
*/
export const configureDefaultEditor = (options = {}) => {
defaultOptions = options;
};
+64
View File
@@ -0,0 +1,64 @@
// 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 Loader for Moodle
*
* @module editor_tiny/loader
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
let tinyMCEPromise;
import * as Config from 'core/config';
export const baseUrl = `${Config.wwwroot}/lib/editor/tiny/loader.php/${M.cfg.jsrev}`;
/**
* Get the TinyMCE API Object.
*
* @returns {Promise<TinyMCE>} The TinyMCE API Object
*/
export const getTinyMCE = () => {
if (tinyMCEPromise) {
return tinyMCEPromise;
}
tinyMCEPromise = new Promise((resolve, reject) => {
const head = document.querySelector('head');
let script = head.querySelector('script[data-tinymce="tinymce"]');
if (script) {
resolve(window.tinyMCE);
}
script = document.createElement('script');
script.dataset.tinymce = 'tinymce';
script.src = `${baseUrl}/tinymce.js`;
script.async = true;
script.addEventListener('load', () => {
resolve(window.tinyMCE);
}, false);
script.addEventListener('error', (err) => {
reject(err);
}, false);
head.append(script);
});
return tinyMCEPromise;
};
+128
View File
@@ -0,0 +1,128 @@
// 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/>.
/**
* Option helper for TinyMCE Editor Manager.
*
* @module editor_tiny/options
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const optionContextId = 'moodle:contextid';
const optionDraftItemId = 'moodle:draftitemid';
const filePickers = 'moodle:filepickers';
const optionsMoodleLang = 'moodle:language';
const currentLanguage = 'moodle:currentLanguage';
const optionPlaceholderSelectors = 'moodle:placeholderSelectors';
export const register = (editor, options) => {
const registerOption = editor.options.register;
const setOption = editor.options.set;
registerOption(optionContextId, {
processor: 'number',
"default": 0,
});
setOption(optionContextId, options.context);
registerOption(filePickers, {
processor: 'object',
"default": {},
});
setOption(filePickers, Object.assign({}, options.filepicker));
registerOption(optionDraftItemId, {
processor: 'number',
"default": 0,
});
setOption(optionDraftItemId, options.draftitemid);
registerOption(currentLanguage, {
processor: 'string',
"default": 'en',
});
setOption(currentLanguage, options.currentLanguage);
// This is primarily used by the media plugin, but it may be re-used elsewhere so is included here as it is large.
registerOption(optionsMoodleLang, {
processor: 'object',
"default": {},
});
setOption(optionsMoodleLang, options.language);
registerOption(optionPlaceholderSelectors, {
processor: 'array',
"default": [],
});
setOption(optionPlaceholderSelectors, options.placeholderSelectors);
};
export const getContextId = (editor) => editor.options.get(optionContextId);
export const getDraftItemId = (editor) => editor.options.get(optionDraftItemId);
export const getFilepickers = (editor) => editor.options.get(filePickers);
export const getFilePicker = (editor, type) => getFilepickers(editor)[type];
export const getMoodleLang = (editor) => editor.options.get(optionsMoodleLang);
export const getCurrentLanguage = (editor) => editor.options.get(currentLanguage);
/**
* Get a set of namespaced options for all defined plugins.
*
* @param {object} options
* @returns {object}
*/
export const getInitialPluginConfiguration = (options) => {
const config = {};
Object.entries(options.plugins).forEach(([pluginName, pluginConfig]) => {
const values = Object.entries(pluginConfig.config ?? {});
values.forEach(([optionName, value]) => {
config[getPluginOptionName(pluginName, optionName)] = value;
});
});
return config;
};
/**
* Get the namespaced option name for a plugin.
*
* @param {string} pluginName
* @param {string} optionName
* @returns {string}
*/
export const getPluginOptionName = (pluginName, optionName) => `${pluginName}:${optionName}`;
/**
* Get the placeholder selectors.
*
* @param {TinyMCE} editor
* @returns {array}
*/
export const getPlaceholderSelectors = (editor) => editor.options.get(optionPlaceholderSelectors);
/**
* Register placeholder selectos.
*
* @param {TinyMCE} editor
* @param {array} selectors
*/
export const registerPlaceholderSelectors = (editor, selectors) => {
if (selectors.length) {
let existingData = getPlaceholderSelectors(editor);
existingData = existingData.concat(selectors);
editor.options.set(optionPlaceholderSelectors, existingData);
}
};
+131
View File
@@ -0,0 +1,131 @@
// 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 for Moodle.
*
* @module editor_tiny/uploader
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {
notifyUploadStarted,
notifyUploadCompleted,
} from 'core_form/events';
import {getFilePicker} from 'editor_tiny/options';
// This image uploader is based on advice given at:
// https://www.tiny.cloud/docs/tinymce/6/upload-images/
export default (editor, filePickerType, blob, fileName, progress) => new Promise((resolve, reject) => {
notifyUploadStarted(editor.targetElm.id);
const xhr = new XMLHttpRequest();
// Add the progress handler.
xhr.upload.addEventListener('progress', (e) => {
progress(e.loaded / e.total * 100);
});
xhr.addEventListener('load', () => {
if (xhr.status === 403) {
reject({
message: `HTTP error: ${xhr.status}`,
remove: true,
});
return;
}
if (xhr.status < 200 || xhr.status >= 300) {
reject(`HTTP Error: ${xhr.status}`);
return;
}
const response = JSON.parse(xhr.responseText);
if (!response) {
reject(`Invalid JSON: ${xhr.responseText}`);
return;
}
notifyUploadCompleted(editor.targetElm.id);
let location;
if (response.url) {
location = response.url;
} else if (response.event && response.event === 'fileexists' && response.newfile) {
// A file with this name is already in use here - rename to avoid conflict.
// Chances are, it's a different image (stored in a different folder on the user's computer).
// If the user wants to reuse an existing image, they can copy/paste it within the editor.
location = response.newfile.url;
}
if (location && typeof location === 'string') {
resolve(location);
return;
}
// Try to parse the error response into a JSON object.
const errorString = xhr.responseText;
let output = '';
try {
output = JSON.parse(errorString);
} catch (error) {
// If the JSON parsing process returns an error, then it returns the original.
output = errorString;
}
reject(output);
});
xhr.addEventListener('error', () => {
reject({
message: `Upload failed due to an XHR transport error. Code: ${xhr.status}`,
remove: true,
});
});
const formData = new FormData();
const options = getFilePicker(editor, filePickerType);
formData.append('repo_upload_file', blob, fileName);
formData.append('itemid', options.itemid);
Object.values(options.repositories).some((repository) => {
if (repository.type === 'upload') {
formData.append('repo_id', repository.id);
return true;
}
return false;
});
formData.append('env', options.env);
formData.append('sesskey', M.cfg.sesskey);
formData.append('client_id', options.client_id);
formData.append('savepath', options.savepath ?? '/');
formData.append('ctx_id', options.context.id);
// Accepted types can be either a string or an array, but an array is
// expected in the processing script, so make sure we are sending an array.
const acceptedTypes = options.accepted_types;
if (Array.isArray(acceptedTypes)) {
acceptedTypes.forEach(function(type) {
formData.append('accepted_types[]', type);
});
} else {
formData.append('accepted_types[]', acceptedTypes);
}
xhr.open('POST', `${M.cfg.wwwroot}/repository/repository_ajax.php?action=upload`, true);
xhr.send(formData);
});
+406
View File
@@ -0,0 +1,406 @@
// 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 {renderForPromise} from 'core/templates';
import {getFilePicker} from './options';
import {getString} from 'core/str';
/**
* Get the image path for the specified image.
*
* @param {string} identifier The name of the image
* @param {string} component The component name
* @return {string} The image URL path
*/
export const getImagePath = (identifier, component = 'editor_tiny') => Promise.resolve(M.util.image_url(identifier, component));
export const getButtonImage = async(identifier, component = 'editor_tiny') => renderForPromise('editor_tiny/toolbar_button', {
image: await getImagePath(identifier, component),
});
/**
* Helper to display a filepicker and return a Promise.
*
* The Promise will resolve when a file is selected, or reject if the file type is not found.
*
* @param {TinyMCE} editor
* @param {string} filetype
* @returns {Promise<object>} The file object returned by the filepicker
*/
export const displayFilepicker = (editor, filetype) => new Promise((resolve, reject) => {
const configuration = getFilePicker(editor, filetype);
if (configuration) {
const options = {
...configuration,
formcallback: resolve,
};
M.core_filepicker.show(Y, options);
return;
}
reject(`Unknown filetype ${filetype}`);
});
/**
* Given a TinyMCE Toolbar configuration, add the specified button to the named section.
*
* @param {object} toolbar
* @param {string} section
* @param {string} button
* @param {string|null} [after=null]
* @returns {object} The toolbar configuration
*/
export const addToolbarButton = (toolbar, section, button, after = null) => {
if (!toolbar) {
return [{
name: section,
items: [button],
}];
}
const mutatedToolbar = JSON.parse(JSON.stringify(toolbar));
return mutatedToolbar.map((item) => {
if (item.name === section) {
if (after) {
// Insert new button after the specified button.
let index = item.items.findIndex(value => value == after);
if (index !== -1) {
item.items.splice(index + 1, 0, button);
}
} else {
// Append button to end of button section.
item.items.push(button);
}
}
return item;
});
};
/**
* Given a TinyMCE Toolbar configuration, add the specified buttons to the named section.
*
* @param {object} toolbar
* @param {string} section
* @param {Array} buttons
* @returns {object} The toolbar configuration
*/
export const addToolbarButtons = (toolbar, section, buttons) => {
if (!toolbar) {
return [{
name: section,
items: buttons,
}];
}
const mutatedToolbar = JSON.parse(JSON.stringify(toolbar));
return mutatedToolbar.map((item) => {
if (item.name === section) {
buttons.forEach(button => item.items.push(button));
}
return item;
});
};
/**
* Insert a new section into the toolbar.
*
* @param {array} toolbar The TinyMCE.editor.settings.toolbar configuration
* @param {string} name The new section name to add
* @param {string} relativeTo Insert relative to this section name
* @param {boolean} append Append or Prepend
* @returns {array}
*/
export const addToolbarSection = (toolbar, name, relativeTo, append = true) => {
const newSection = {
name,
items: [],
};
const sectionInserted = toolbar.some((section, index) => {
if (section.name === relativeTo) {
if (append) {
toolbar.splice(index + 1, 0, newSection);
} else {
toolbar.splice(index, 0, newSection);
}
return true;
}
return false;
});
if (!sectionInserted) {
// Relative section not found.
if (append) {
toolbar.push(newSection);
} else {
toolbar.unshift(newSection);
}
}
return toolbar;
};
/**
* Given a TinyMCE Menubar configuration, add the specified button to the named section.
*
* @param {object} menubar
* @param {string} section
* @param {string} menuitem
* @param {string|null} [after=null]
* @returns {object}
*/
export const addMenubarItem = (menubar, section, menuitem, after = null) => {
if (!menubar) {
const emptyMenubar = {};
emptyMenubar[section] = {
title: section,
items: menuitem,
};
}
const mutatedMenubar = JSON.parse(JSON.stringify(menubar));
Array.from(Object.entries(mutatedMenubar)).forEach(([name, menu]) => {
if (name === section) {
if (after) {
// Insert new item after the specified menu item.
let index = menu.items.indexOf(after);
if (index !== -1) {
index += after.length;
menu.items = menu.items.slice(0, index) + ` ${menuitem}` + menu.items.slice(index);
}
} else {
// Append item to end of the menu section.
menu.items = `${menu.items} ${menuitem}`;
}
}
});
return mutatedMenubar;
};
/**
* Given a TinyMCE contextmenu configuration, add the specified button to the end.
*
* @param {string} contextmenu
* @param {string[]} menuitems
* @returns {string}
*/
export const addContextmenuItem = (contextmenu, ...menuitems) => {
const contextmenuItems = (contextmenu ? contextmenu : '').split(' ');
return contextmenuItems
.concat(menuitems)
.filter((item) => item !== '')
.join(' ');
};
/**
* Given a TinyMCE quickbars configuration, add items to the menu.
*
* @param {string} quicktoolbar
* @param {string[]} toolbaritems
* @returns {string}
*/
export const addQuickbarsToolbarItem = (quicktoolbar, ...toolbaritems) => {
const quicktoolbarItems = (quicktoolbar ? quicktoolbar : '').split(' ');
return quicktoolbarItems
.concat(toolbaritems)
.filter((item) => item !== '')
.join(' ');
};
/**
* Get the link to the user documentation for the named plugin.
*
* @param {string} pluginName
* @returns {string}
*/
export const getDocumentationLink = (pluginName) => `https://docs.moodle.org/en/editor_tiny/${pluginName}`;
/**
* Get the default plugin metadata for the named plugin.
* If no URL is provided, then a URL is generated pointing to the standard Moodle Documentation.
*
* @param {string} component The component name
* @param {string} pluginName The plugin name
* @param {string|null} [url=null] An optional URL to the plugin documentation
* @returns {object}
*/
export const getPluginMetadata = async(component, pluginName, url = null) => {
const name = await getString('helplinktext', component);
return {
getMetadata: () => ({
name,
url: url ?? getDocumentationLink(pluginName),
}),
};
};
/**
* Ensure that the editor is still in the DOM, removing it if it is not.
*
* @param {TinyMCE} editor
* @returns {TinyMCE|null}
*/
export const ensureEditorIsValid = (editor) => {
// TinyMCE uses the element ID as a map key internally, even if the target has changed.
// In cases such as where an editor is in a modal form which has been detached from the DOM, but the editor not removed,
// we need to manually destroy the editor.
// We could theoretically do this with a Mutation Observer, but in some cases the Node may be moved,
// or added back elsewhere in the DOM.
if (!editor.getElement().isConnected) {
return null;
}
return editor;
};
/**
* Given a TinyMCE Toolbar configuration, remove the specified button from the named section.
*
* @param {object} toolbar
* @param {string} section
* @param {string} button
* @returns {object} The toolbar configuration
*/
export const removeToolbarButton = (toolbar, section, button) => {
if (!toolbar) {
return [{
name: section,
items: [button],
}];
}
const mutatedToolbar = JSON.parse(JSON.stringify(toolbar));
return mutatedToolbar.map((item) => {
if (item.name === section) {
item.items.splice(item.items.indexOf(button), 1);
}
return item;
});
};
/**
* Given a TinyMCE Toolbar configuration, remove the specified buttons from the named section.
*
* @param {object} toolbar
* @param {string} section
* @param {Array} buttons
* @returns {object} The toolbar configuration
*/
export const removeToolbarButtons = (toolbar, section, buttons) => {
if (!toolbar) {
return [{
name: section,
items: buttons,
}];
}
const mutatedToolbar = JSON.parse(JSON.stringify(toolbar));
return mutatedToolbar.map((item) => {
if (item.name === section) {
buttons.forEach(button => item.items.splice(item.items.indexOf(button), 1));
}
return item;
});
};
/**
* Remove the specified sub-menu item from the named section.
* Recreate a menu with the same sub-menu items but remove the specified item.
*
* @param {TinyMCE} editor
* @param {string} section
* @param {string} submenuitem The text of sub-menu that we want to removed
*/
export const removeSubmenuItem = async(editor, section, submenuitem) => {
// Get menu items.
const menuItems = editor.ui.registry.getAll().menuItems[section];
// Because we will match between title strings,
// we make sure no problems arise while applying multi-language.
const submenuitemtitle = await getString(submenuitem, 'editor_tiny');
// Overriding the menu items,
// by recreating them but excluding the specified sub-menu.
if (menuItems) {
editor.ui.registry.addNestedMenuItem(
section,
{
text: menuItems.text,
getSubmenuItems: () => {
let newSubmenu = [];
menuItems.getSubmenuItems().forEach((item) => {
// Need to trim the text because some of the sub-menus use space to replace an icon.
if (item.text.trim() != submenuitemtitle) {
newSubmenu.push(item);
}
});
return newSubmenu;
}
}
);
}
};
/**
* Given a TinyMCE Menubar configuration, remove the specified menu from the named section.
*
* @param {string} menubar
* @param {string} section
* @param {string} menuitem
* @returns {object}
*/
export const removeMenubarItem = (menubar, section, menuitem) => {
menubar[section].items = menubar[section].items
.replace(menuitem, '');
return menubar;
};
/**
* Given a TinyMCE Menubar configuration, remove the specified menu from the named section.
*
* @param {string} menubar
* @param {string} section
* @param {Array} menuitems
* @returns {object}
*/
export const removeMenubarItems = (menubar, section, menuitems) => {
// Create RegExp pattern.
const regexPattern = new RegExp(menuitems.join('|'), "ig");
// Remove menuitems.
menubar[section].items = menubar[section].items.replace(regexPattern, '');
return menubar;
};
/**
* Updates the state of the editor.
*
* @param {TinyMCE} editor
* @param {HTMLElement} target
*/
export const updateEditorState = (editor, target) => {
if (target.hasAttribute('readonly')) {
editor.mode.set("readonly");
} else {
editor.mode.set("design");
}
};