diff --git a/lib/editor/tiny/amd/build/editor.min.js b/lib/editor/tiny/amd/build/editor.min.js index 5e45115cd47..0dcc52257c9 100644 --- a/lib/editor/tiny/amd/build/editor.min.js +++ b/lib/editor/tiny/amd/build/editor.min.js @@ -1,3 +1,3 @@ -define("editor_tiny/editor",["exports","jquery","core/pending","./defaults","./loader","./options","./utils"],(function(_exports,_jquery,_pending,_defaults,_loader,Options,_utils){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setupForTarget=_exports.setupForElementId=_exports.getInstanceForElementId=_exports.getInstanceForElement=_exports.getAllInstances=_exports.configureDefaultEditor=void 0,_jquery=_interopRequireDefault(_jquery),_pending=_interopRequireDefault(_pending),Options=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}(Options);var _systemImportTransformerGlobalIdentifier="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};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 _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}const instanceMap=new Map;let defaultOptions={};const importPluginList=async pluginList=>{const pluginHandlers=await Promise.all(pluginList.map((pluginPath=>-1===pluginPath.indexOf("/")?Promise.resolve(pluginPath):"function"==typeof _systemImportTransformerGlobalIdentifier.define&&_systemImportTransformerGlobalIdentifier.define.amd?new Promise((function(resolve,reject){_systemImportTransformerGlobalIdentifier.require([pluginPath],resolve,reject)})):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&_systemImportTransformerGlobalIdentifier.require&&"component"===_systemImportTransformerGlobalIdentifier.require.loader?Promise.resolve(require(pluginPath)):Promise.resolve(_systemImportTransformerGlobalIdentifier[pluginPath])))),pluginNames=pluginHandlers.map((pluginConfig=>"string"==typeof pluginConfig?pluginConfig:Array.isArray(pluginConfig)?pluginConfig[0]:null)).filter((value=>value));return{pluginNames:pluginNames,pluginConfig:pluginHandlers.map((pluginConfig=>Array.isArray(pluginConfig)?pluginConfig[1]:null)).filter((value=>value))}};_exports.getAllInstances=()=>new Map(instanceMap.entries());_exports.getInstanceForElementId=elementId=>getInstanceForElement(document.getElementById(elementId));const getInstanceForElement=element=>{const instance=instanceMap.get(element);if(!instance||!instance.removed)return instance;instanceMap.delete(element)};_exports.getInstanceForElement=getInstanceForElement;_exports.setupForElementId=_ref=>{let{elementId:elementId,options:options}=_ref;const target=document.getElementById(elementId);setTimeout((()=>setupForTarget(target,options)),1)};(async()=>{const lang=document.querySelector("html").lang,[tinyMCE,langData]=await Promise.all([(0,_loader.getTinyMCE)(),(language=lang,fetch("".concat(M.cfg.wwwroot,"/lib/editor/tiny/lang.php/").concat(M.cfg.langrev,"/").concat(language)).then((response=>response.json())))]);var language;tinyMCE.addI18n(lang,langData)})();const getPlugins=function(){let{plugins:plugins=null}=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return plugins||(defaultOptions.plugins?defaultOptions.plugins:{})},getStandardConfig=(target,tinyMCE,options,plugins)=>{const lang=document.querySelector("html").lang,config=Object.assign({},(0,_defaults.getDefaultConfiguration)(),{base_url:_loader.baseUrl,target:target,min_height:175,height:target.clientHeight||"auto",language:lang,content_css:[options.css],convert_urls:!1,a11y_advanced_options:!0,extended_valid_elements:"script[*],p[*],i[*]",xss_sanitization:!1,quickbars_insert_toolbar:"",quickbars_selection_toolbar:target.rows>5&&(0,_defaults.getDefaultQuickbarsSelectionToolbar)(),block_formats:"Paragraph=p;Heading 3=h3;Heading 4=h4;Heading 5=h5;Heading 6=h6;Preformatted=pre;Blockquote=blockquote",plugins:[...plugins],skin:"oxide",help_accessibility:!1,promotion:!1,branding:options.branding,table_header_type:"sectionCells",entity_encoding:"raw",ui_mode:"split",browser_spellcheck:!0,setup:editor=>{Options.register(editor,options),editor.on("PreInit",(function(){this.contentWindow=this.iframeElement.contentWindow})),editor.on("init",(function(){(0,_utils.removeSubmenuItem)(editor,"align","tiny:justify"),((editor,target)=>{let expectedEditingAreaHeight=0;expectedEditingAreaHeight=target.clientHeight?target.clientHeight:target.rows*(parseFloat(window.getComputedStyle(target).lineHeight)||22),editor.getContainer().querySelector(".tox-sidebar-wrap").clientHeight{const{pluginNames:pluginNames,pluginConfig:pluginConfig}=pluginValues,instanceConfig=getStandardConfig(target,0,options,pluginNames);return instanceConfig.menu.file&&(instanceConfig.menu.file.items=""),instanceConfig.menu.format&&(instanceConfig.menu.format.items=instanceConfig.menu.format.items.replace(/forecolor ?/,"").replace(/backcolor ?/,"").replace(/fontfamily ?/,"").replace(/fontsize ?/,"").replace(/styles ?/,"").replaceAll(/\| *\|/g,"|")),!1!==instanceConfig.quickbars_selection_toolbar&&(instanceConfig.quickbars_selection_toolbar=instanceConfig.quickbars_selection_toolbar.replace("h2 h3","h3 h4 h5 h6")),pluginConfig.filter((pluginConfig=>"function"==typeof pluginConfig.configure)).forEach((pluginConfig=>{const pluginInstanceOverride=pluginConfig.configure(instanceConfig,options);Object.assign(instanceConfig,pluginInstanceOverride)})),Object.assign(instanceConfig,Options.getInitialPluginConfiguration(options)),instanceConfig},isModalMode=target=>!!target.closest('[data-region="modal"]'),setupForTarget=async function(target){let options=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const instance=getInstanceForElement(target);if(instance)return Promise.resolve(instance);const pendingPromise=new _pending.default("editor_tiny/editor:setupForTarget"),plugins=getPlugins(options),[tinyMCE,pluginValues]=await Promise.all([(0,_loader.getTinyMCE)(),importPluginList(Object.keys(plugins))]);tinyMCE.get().filter((editor=>!editor.getElement().isConnected)).forEach((editor=>{editor.remove()}));const existingEditor=tinyMCE.EditorManager.get(target.id);if(existingEditor){if(existingEditor.getElement()===target)return pendingPromise.resolve(),Promise.resolve(existingEditor);throw pendingPromise.resolve(),new Error("TinyMCE instance already exists for different target with same ID")}const instanceConfig=getEditorConfiguration(target,0,options,pluginValues),[editor]=await tinyMCE.init(instanceConfig);return target.dataset.fieldtype="editor",instanceMap.set(target,editor),editor.on("remove",(_ref2=>{let{target:target}=_ref2;instanceMap.delete(target.targetElm),target.targetElm.dataset.fieldtype=null})),target.form&&(0,_jquery.default)(target.form).on("submit",(()=>{editor.save()})),editor.on("blur",(()=>{editor.save()})),editor.on("OpenWindow",(()=>{const modals=document.querySelectorAll('[data-region="modal"]');modals&&modals.forEach((modal=>{modal.classList.contains("hide")||modal.classList.add("hide")}))})),editor.on("CloseWindow",(()=>{if(isModalMode(target)){const modals=document.querySelectorAll('[data-region="modal"]');modals&&modals.forEach((modal=>{modal.classList.contains("hide")&&modal.classList.remove("hide")}))}})),pendingPromise.resolve(),editor};_exports.setupForTarget=setupForTarget;_exports.configureDefaultEditor=function(){let options=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};defaultOptions=options}})); +define("editor_tiny/editor",["exports","jquery","core/pending","./defaults","./loader","./options","./utils"],(function(_exports,_jquery,_pending,_defaults,_loader,Options,_utils){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setupForTarget=_exports.setupForElementId=_exports.getInstanceForElementId=_exports.getInstanceForElement=_exports.getAllInstances=_exports.configureDefaultEditor=void 0,_jquery=_interopRequireDefault(_jquery),_pending=_interopRequireDefault(_pending),Options=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}(Options);var _systemImportTransformerGlobalIdentifier="undefined"!=typeof window?window:"undefined"!=typeof self?self:"undefined"!=typeof global?global:{};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 _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}const instanceMap=new Map;let defaultOptions={};const importPluginList=async pluginList=>{const pluginHandlers=await Promise.all(pluginList.map((pluginPath=>-1===pluginPath.indexOf("/")?Promise.resolve(pluginPath):"function"==typeof _systemImportTransformerGlobalIdentifier.define&&_systemImportTransformerGlobalIdentifier.define.amd?new Promise((function(resolve,reject){_systemImportTransformerGlobalIdentifier.require([pluginPath],resolve,reject)})):"undefined"!=typeof module&&module.exports&&"undefined"!=typeof require||"undefined"!=typeof module&&module.component&&_systemImportTransformerGlobalIdentifier.require&&"component"===_systemImportTransformerGlobalIdentifier.require.loader?Promise.resolve(require(pluginPath)):Promise.resolve(_systemImportTransformerGlobalIdentifier[pluginPath])))),pluginNames=pluginHandlers.map((pluginConfig=>"string"==typeof pluginConfig?pluginConfig:Array.isArray(pluginConfig)?pluginConfig[0]:null)).filter((value=>value));return{pluginNames:pluginNames,pluginConfig:pluginHandlers.map((pluginConfig=>Array.isArray(pluginConfig)?pluginConfig[1]:null)).filter((value=>value))}};_exports.getAllInstances=()=>new Map(instanceMap.entries());_exports.getInstanceForElementId=elementId=>getInstanceForElement(document.getElementById(elementId));const getInstanceForElement=element=>{const instance=instanceMap.get(element);if(!instance||!instance.removed)return instance;instanceMap.delete(element)};_exports.getInstanceForElement=getInstanceForElement;_exports.setupForElementId=_ref=>{let{elementId:elementId,options:options}=_ref;const target=document.getElementById(elementId);setTimeout((()=>setupForTarget(target,options)),1)};(async()=>{const lang=document.querySelector("html").lang,[tinyMCE,langData]=await Promise.all([(0,_loader.getTinyMCE)(),(language=lang,fetch("".concat(M.cfg.wwwroot,"/lib/editor/tiny/lang.php/").concat(M.cfg.langrev,"/").concat(language)).then((response=>response.json())))]);var language;tinyMCE.addI18n(lang,langData)})();const getPlugins=function(){let{plugins:plugins=null}=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return plugins||(defaultOptions.plugins?defaultOptions.plugins:{})},getStandardConfig=(target,tinyMCE,options,plugins)=>{const lang=document.querySelector("html").lang,config=Object.assign({},(0,_defaults.getDefaultConfiguration)(),{base_url:_loader.baseUrl,target:target,min_height:175,height:target.clientHeight||"auto",language:lang,content_css:[options.css],convert_urls:!1,a11y_advanced_options:!0,extended_valid_elements:"script[*],p[*],i[*]",xss_sanitization:!1,quickbars_insert_toolbar:"",quickbars_selection_toolbar:target.rows>5&&(0,_defaults.getDefaultQuickbarsSelectionToolbar)(),block_formats:"Paragraph=p;Heading 3=h3;Heading 4=h4;Heading 5=h5;Heading 6=h6;Preformatted=pre;Blockquote=blockquote",plugins:[...plugins],skin:"oxide",help_accessibility:!1,promotion:!1,branding:options.branding,table_header_type:"sectionCells",entity_encoding:"raw",ui_mode:"split",browser_spellcheck:!0,license_key:"gpl",setup:editor=>{Options.register(editor,options),editor.on("PreInit",(function(){this.contentWindow=this.iframeElement.contentWindow})),editor.on("init",(function(){(0,_utils.removeSubmenuItem)(editor,"align","tiny:justify"),((editor,target)=>{let expectedEditingAreaHeight=0;expectedEditingAreaHeight=target.clientHeight?target.clientHeight:target.rows*(parseFloat(window.getComputedStyle(target).lineHeight)||22),editor.getContainer().querySelector(".tox-sidebar-wrap").clientHeight{const{pluginNames:pluginNames,pluginConfig:pluginConfig}=pluginValues,instanceConfig=getStandardConfig(target,0,options,pluginNames);return instanceConfig.menu.file&&(instanceConfig.menu.file.items=""),instanceConfig.menu.format&&(instanceConfig.menu.format.items=instanceConfig.menu.format.items.replace(/forecolor ?/,"").replace(/backcolor ?/,"").replace(/fontfamily ?/,"").replace(/fontsize ?/,"").replace(/styles ?/,"").replaceAll(/\| *\|/g,"|")),!1!==instanceConfig.quickbars_selection_toolbar&&(instanceConfig.quickbars_selection_toolbar=instanceConfig.quickbars_selection_toolbar.replace("h2 h3","h3 h4 h5 h6")),pluginConfig.filter((pluginConfig=>"function"==typeof pluginConfig.configure)).forEach((pluginConfig=>{const pluginInstanceOverride=pluginConfig.configure(instanceConfig,options);Object.assign(instanceConfig,pluginInstanceOverride)})),Object.assign(instanceConfig,Options.getInitialPluginConfiguration(options)),instanceConfig},isModalMode=target=>!!target.closest('[data-region="modal"]'),setupForTarget=async function(target){let options=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const instance=getInstanceForElement(target);if(instance)return Promise.resolve(instance);const pendingPromise=new _pending.default("editor_tiny/editor:setupForTarget"),plugins=getPlugins(options),[tinyMCE,pluginValues]=await Promise.all([(0,_loader.getTinyMCE)(),importPluginList(Object.keys(plugins))]);tinyMCE.get().filter((editor=>!editor.getElement().isConnected)).forEach((editor=>{editor.remove()}));const existingEditor=tinyMCE.EditorManager.get(target.id);if(existingEditor){if(existingEditor.getElement()===target)return pendingPromise.resolve(),Promise.resolve(existingEditor);throw pendingPromise.resolve(),new Error("TinyMCE instance already exists for different target with same ID")}const instanceConfig=getEditorConfiguration(target,0,options,pluginValues),[editor]=await tinyMCE.init(instanceConfig);return target.dataset.fieldtype="editor",instanceMap.set(target,editor),editor.on("remove",(_ref2=>{let{target:target}=_ref2;instanceMap.delete(target.targetElm),target.targetElm.dataset.fieldtype=null})),target.form&&(0,_jquery.default)(target.form).on("submit",(()=>{editor.save()})),editor.on("blur",(()=>{editor.save()})),editor.on("OpenWindow",(()=>{const modals=document.querySelectorAll('[data-region="modal"]');modals&&modals.forEach((modal=>{modal.classList.contains("hide")||modal.classList.add("hide")}))})),editor.on("CloseWindow",(()=>{if(isModalMode(target)){const modals=document.querySelectorAll('[data-region="modal"]');modals&&modals.forEach((modal=>{modal.classList.contains("hide")&&modal.classList.remove("hide")}))}})),pendingPromise.resolve(),editor};_exports.setupForTarget=setupForTarget;_exports.configureDefaultEditor=function(){let options=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};defaultOptions=options}})); //# sourceMappingURL=editor.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/amd/build/editor.min.js.map b/lib/editor/tiny/amd/build/editor.min.js.map index 5352a008bbe..18d1ef03edf 100644 --- a/lib/editor/tiny/amd/build/editor.min.js.map +++ b/lib/editor/tiny/amd/build/editor.min.js.map @@ -1 +1 @@ -{"version":3,"file":"editor.min.js","sources":["../src/editor.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * TinyMCE Editor Manager.\n *\n * @module editor_tiny/editor\n * @copyright 2022 Andrew Lyons \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport jQuery from 'jquery';\nimport Pending from 'core/pending';\nimport {getDefaultConfiguration, getDefaultQuickbarsSelectionToolbar} from './defaults';\nimport {getTinyMCE, baseUrl} from './loader';\nimport * as Options from './options';\nimport {addToolbarButton, addToolbarButtons, addToolbarSection,\n removeToolbarButton, removeSubmenuItem, updateEditorState} from './utils';\n\n/**\n * Storage for the TinyMCE instances on the page.\n * @type {Map}\n */\nconst instanceMap = new Map();\n\n/**\n * The default editor configuration.\n * @type {Object}\n */\nlet defaultOptions = {};\n\n/**\n * Require the modules for the named set of TinyMCE plugins.\n *\n * @param {string[]} pluginList The list of plugins\n * @return {Promise[]} A matching set of Promises relating to the requested plugins\n */\nconst importPluginList = async(pluginList) => {\n // Fetch all of the plugins from the list of plugins.\n // If a plugin contains a '/' then it is assumed to be a Moodle AMD module to import.\n const pluginHandlers = await Promise.all(pluginList.map(pluginPath => {\n if (pluginPath.indexOf('/') === -1) {\n // A standard TinyMCE Plugin.\n return Promise.resolve(pluginPath);\n }\n\n return import(pluginPath);\n }));\n\n // Normalise the plugin data to a list of plugin names.\n // Two formats are supported:\n // - a string; and\n // - an array whose first element is the plugin name, and the second element is the plugin configuration.\n const pluginNames = pluginHandlers.map((pluginConfig) => {\n if (typeof pluginConfig === 'string') {\n return pluginConfig;\n }\n if (Array.isArray(pluginConfig)) {\n return pluginConfig[0];\n }\n return null;\n }).filter((value) => value);\n\n // Fetch the list of pluginConfig handlers.\n const pluginConfig = pluginHandlers.map((pluginConfig) => {\n if (Array.isArray(pluginConfig)) {\n return pluginConfig[1];\n }\n return null;\n }).filter((value) => value);\n\n return {\n pluginNames,\n pluginConfig,\n };\n};\n\n/**\n * Fetch the language data for the specified language.\n *\n * @param {string} language The language identifier\n * @returns {object}\n */\nconst fetchLanguage = (language) => fetch(\n `${M.cfg.wwwroot}/lib/editor/tiny/lang.php/${M.cfg.langrev}/${language}`\n).then(response => response.json());\n\n/**\n * Get a list of all Editors in a Map, keyed by the DOM Node that the Editor is associated with.\n *\n * @returns {Map}\n */\nexport const getAllInstances = () => new Map(instanceMap.entries());\n\n/**\n * Get the TinyMCE instance for the specified Node ID.\n *\n * @param {string} elementId\n * @returns {TinyMCE|undefined}\n */\nexport const getInstanceForElementId = elementId => getInstanceForElement(document.getElementById(elementId));\n\n/*\n * Get the TinyMCE instance for the specified HTMLElement.\n *\n * @param {HTMLElement} element\n * @returns {TinyMCE|undefined}\n */\nexport const getInstanceForElement = element => {\n const instance = instanceMap.get(element);\n if (instance && instance.removed) {\n instanceMap.delete(element);\n return undefined;\n }\n return instance;\n};\n\n/**\n * Set up TinyMCE for the selector at the specified HTML Node id.\n *\n * @param {object} config The configuration required to setup the editor\n * @param {string} config.elementId The HTML Node ID\n * @param {Object} config.options The editor plugin configuration\n */\nexport const setupForElementId = ({elementId, options}) => {\n const target = document.getElementById(elementId);\n // We will need to wrap the setupForTarget and editor.remove() calls in a setTimeout.\n // Because other events callbacks will still try to run on the removed instance.\n // This will cause an error on Firefox.\n // We need to make TinyMCE to remove itself outside the event loop.\n // @see https://github.com/tinymce/tinymce/issues/3129 for more details.\n setTimeout(() => {\n return setupForTarget(target, options);\n }, 1);\n};\n\n/**\n * Initialise the page with standard TinyMCE requirements.\n *\n * Currently this includes the language taken from the HTML lang property.\n */\nconst initialisePage = async() => {\n const lang = document.querySelector('html').lang;\n\n const [tinyMCE, langData] = await Promise.all([getTinyMCE(), fetchLanguage(lang)]);\n tinyMCE.addI18n(lang, langData);\n};\ninitialisePage();\n\n/**\n * Get the list of plugins to load for the specified configuration.\n *\n * If the specified configuration does not include a plugin configuration, then return the default configuration.\n *\n * @param {object} options\n * @param {array} [options.plugins=null] The plugin list\n * @returns {object}\n */\nconst getPlugins = ({plugins = null} = {}) => {\n if (plugins) {\n return plugins;\n }\n\n if (defaultOptions.plugins) {\n return defaultOptions.plugins;\n }\n\n return {};\n};\n\n/**\n * Adjust the editor size base on the target element.\n *\n * @param {TinyMCE} editor TinyMCE editor\n * @param {Node} target Target element\n */\nconst adjustEditorSize = (editor, target) => {\n let expectedEditingAreaHeight = 0;\n if (target.clientHeight) {\n expectedEditingAreaHeight = target.clientHeight;\n } else {\n // If the target element is hidden, we cannot get the lineHeight of the target element.\n // 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.\n expectedEditingAreaHeight = target.rows * (parseFloat(window.getComputedStyle(target).lineHeight) || 22);\n }\n const currentEditingAreaHeight = editor.getContainer().querySelector('.tox-sidebar-wrap').clientHeight;\n if (currentEditingAreaHeight < expectedEditingAreaHeight) {\n // Change the height based on the target element's height.\n editor.getContainer().querySelector('.tox-sidebar-wrap').style.height = `${expectedEditingAreaHeight}px`;\n }\n};\n\n/**\n * Get the standard configuration for the specified options.\n *\n * @param {Node} target\n * @param {tinyMCE} tinyMCE\n * @param {object} options\n * @param {Array} plugins\n * @returns {object}\n */\nconst getStandardConfig = (target, tinyMCE, options, plugins) => {\n const lang = document.querySelector('html').lang;\n\n const config = Object.assign({}, getDefaultConfiguration(), {\n // eslint-disable-next-line camelcase\n base_url: baseUrl,\n\n // Set the editor target.\n // https://www.tiny.cloud/docs/tinymce/6/editor-important-options/#target\n target,\n\n // https://www.tiny.cloud/docs/tinymce/6/customize-ui/#set-maximum-and-minimum-heights-and-widths\n // Set the minimum height to the smallest height that we can fit the Menu bar, Tool bar, Status bar and the text area.\n // eslint-disable-next-line camelcase\n min_height: 175,\n\n // Base the height on the size of the text area.\n // In some cases, E.g.: The target is an advanced element, it will be hidden. We cannot get the height at this time.\n // So set the height to auto, and adjust it later by adjustEditorSize().\n height: target.clientHeight || 'auto',\n\n // Set the language.\n // https://www.tiny.cloud/docs/tinymce/6/ui-localization/#language\n // eslint-disable-next-line camelcase\n language: lang,\n\n // Load the editor stylesheet into the editor iframe.\n // https://www.tiny.cloud/docs/tinymce/6/add-css-options/\n // eslint-disable-next-line camelcase\n content_css: [\n options.css,\n ],\n\n // Do not convert URLs to relative URLs.\n // https://www.tiny.cloud/docs/tinymce/6/url-handling/#convert_urls\n // eslint-disable-next-line camelcase\n convert_urls: false,\n\n // Enabled 'advanced' a11y options.\n // This includes allowing role=\"presentation\" from the image uploader.\n // https://www.tiny.cloud/docs/tinymce/6/accessibility/\n // eslint-disable-next-line camelcase\n a11y_advanced_options: true,\n\n // Add specific rules to the valid elements.\n // eslint-disable-next-line camelcase\n extended_valid_elements: 'script[*],p[*],i[*]',\n\n // Disable XSS Sanitisation.\n // We do this in PHP.\n // https://www.tiny.cloud/docs/tinymce/6/security/#turning-dompurify-off\n // Note: This feature has been backported from TinyMCE 6.4.0.\n // eslint-disable-next-line camelcase\n xss_sanitization: false,\n\n // Disable quickbars entirely.\n // The UI is not ideal and we'll wait for it to improve in future before we enable it in Moodle.\n // eslint-disable-next-line camelcase\n quickbars_insert_toolbar: '',\n\n // If the target element is too small, disable the quickbars selection toolbar.\n // The quickbars selection toolbar is not displayed correctly if the target element is too small.\n // See: https://github.com/tinymce/tinymce/issues/9693.\n quickbars_selection_toolbar: target.rows > 5 ? getDefaultQuickbarsSelectionToolbar() : false,\n\n // Override the standard block formats property (removing h1 & h2).\n // https://www.tiny.cloud/docs/tinymce/6/user-formatting-options/#block_formats\n // eslint-disable-next-line camelcase\n block_formats: 'Paragraph=p;Heading 3=h3;Heading 4=h4;Heading 5=h5;Heading 6=h6;Preformatted=pre;Blockquote=blockquote',\n\n // The list of plugins to include in the instance.\n // https://www.tiny.cloud/docs/tinymce/6/editor-important-options/#plugins\n plugins: [\n ...plugins,\n ],\n\n // Skins\n skin: 'oxide',\n\n // Do not show the help link in the status bar.\n // https://www.tiny.cloud/docs/tinymce/latest/accessibility/#help_accessibility\n // eslint-disable-next-line camelcase\n help_accessibility: false,\n\n // Remove the \"Upgrade\" link for Tiny.\n // https://www.tiny.cloud/docs/tinymce/6/editor-premium-upgrade-promotion/\n promotion: false,\n\n // Allow the administrator to disable branding.\n // https://www.tiny.cloud/docs/tinymce/6/statusbar-configuration-options/#branding\n branding: options.branding,\n\n // Put th cells in a thead element.\n // https://www.tiny.cloud/docs/tinymce/6/table-options/#table_header_type\n // eslint-disable-next-line camelcase\n table_header_type: 'sectionCells',\n\n // Stored text in non-entity form.\n // https://www.tiny.cloud/docs/tinymce/6/content-filtering/#entity_encoding\n // eslint-disable-next-line camelcase\n entity_encoding: \"raw\",\n\n // Enable support for editors in scrollable containers.\n // https://www.tiny.cloud/docs/tinymce/6/ui-mode-configuration-options/#ui_mode\n // eslint-disable-next-line camelcase\n ui_mode: 'split',\n\n // Enable browser-supported spell checking.\n // https://www.tiny.cloud/docs/tinymce/latest/spelling/\n // eslint-disable-next-line camelcase\n browser_spellcheck: true,\n\n setup: (editor) => {\n Options.register(editor, options);\n\n editor.on('PreInit', function() {\n // Work around a bug in TinyMCE with Firefox.\n // When an editor is removed, and replaced with an identically attributed editor (same ID),\n // and the Firefox window is freshly opened (e.g. Behat, Private browsing), the wrong contentWindow\n // is assigned to the editor instance leading to an NS_ERROR_UNEXPECTED error in Firefox.\n // This is a workaround for that issue.\n this.contentWindow = this.iframeElement.contentWindow;\n });\n editor.on('init', function() {\n // Hide justify alignment sub-menu.\n removeSubmenuItem(editor, 'align', 'tiny:justify');\n // Adjust the editor size.\n adjustEditorSize(editor, target);\n });\n\n target.addEventListener('form:editorUpdated', function() {\n updateEditorState(editor, target);\n });\n\n target.dispatchEvent(new Event('form:editorUpdated'));\n },\n });\n\n config.toolbar = addToolbarSection(config.toolbar, 'content', 'formatting', true);\n config.toolbar = addToolbarButton(config.toolbar, 'content', 'link');\n\n // Add directionality plugins, always.\n config.toolbar = addToolbarSection(config.toolbar, 'directionality', 'alignment', true);\n config.toolbar = addToolbarButtons(config.toolbar, 'directionality', ['ltr', 'rtl']);\n\n // Remove the align justify button from the toolbar.\n config.toolbar = removeToolbarButton(config.toolbar, 'alignment', 'alignjustify');\n\n return config;\n};\n\n/**\n * Fetch the TinyMCE configuration for this editor instance.\n *\n * @param {HTMLElement} target\n * @param {TinyMCE} tinyMCE The TinyMCE API\n * @param {Object} options The editor plugin configuration\n * @param {object} pluginValues\n * @param {object} pluginValues.pluginConfig The list of plugin configuration\n * @param {object} pluginValues.pluginNames The list of plugins to load\n * @returns {object} The TinyMCE Configuration\n */\nconst getEditorConfiguration = (target, tinyMCE, options, pluginValues) => {\n const {\n pluginNames,\n pluginConfig,\n } = pluginValues;\n\n // Allow plugins to modify the configuration.\n // This seems a little strange, but we must double-process the config slightly.\n\n // First we fetch the standard configuration.\n const instanceConfig = getStandardConfig(target, tinyMCE, options, pluginNames);\n\n // Next we make any standard changes.\n // Here we remove the file menu, as it doesn't offer any useful functionality.\n // We only empty the items list so that a plugin may choose to add to it themselves later if they wish.\n if (instanceConfig.menu.file) {\n instanceConfig.menu.file.items = '';\n }\n\n // We disable the styles, backcolor, and forecolor plugins from the format menu.\n // These are not useful for Moodle and we don't want to encourage their use.\n if (instanceConfig.menu.format) {\n instanceConfig.menu.format.items = instanceConfig.menu.format.items\n // Remove forecolor and backcolor.\n .replace(/forecolor ?/, '')\n .replace(/backcolor ?/, '')\n\n // Remove fontfamily for now.\n .replace(/fontfamily ?/, '')\n\n // Remove fontsize for now.\n .replace(/fontsize ?/, '')\n\n // Remove styles - it just duplicates the format menu in a way which does not respect configuration\n .replace(/styles ?/, '')\n\n // Remove any duplicate separators.\n .replaceAll(/\\| *\\|/g, '|');\n }\n\n if (instanceConfig.quickbars_selection_toolbar !== false) {\n // eslint-disable-next-line camelcase\n instanceConfig.quickbars_selection_toolbar = instanceConfig.quickbars_selection_toolbar.replace('h2 h3', 'h3 h4 h5 h6');\n }\n\n // Next we call the `configure` function for any plugin which defines it.\n // We pass the current instanceConfig in here, to allow them to make certain changes to the global configuration.\n // For example, to add themselves to any menu, toolbar, and so on.\n // Any plugin which wishes to have configuration options must register those options here.\n pluginConfig.filter((pluginConfig) => typeof pluginConfig.configure === 'function').forEach((pluginConfig) => {\n const pluginInstanceOverride = pluginConfig.configure(instanceConfig, options);\n Object.assign(instanceConfig, pluginInstanceOverride);\n });\n\n // Next we convert the plugin configuration into a format that TinyMCE understands.\n Object.assign(instanceConfig, Options.getInitialPluginConfiguration(options));\n\n return instanceConfig;\n};\n\n/**\n * Check if the target for TinyMCE is in a modal or not.\n *\n * @param {HTMLElement} target Target to check\n * @returns {boolean} True if the target is in a modal form.\n */\nconst isModalMode = (target) => {\n return !!target.closest('[data-region=\"modal\"]');\n};\n\n/**\n * Set up TinyMCE for the HTML Element.\n *\n * @param {HTMLElement} target\n * @param {Object} [options={}] The editor plugin configuration\n * @return {Promise} The TinyMCE instance\n */\nexport const setupForTarget = async(target, options = {}) => {\n const instance = getInstanceForElement(target);\n if (instance) {\n return Promise.resolve(instance);\n }\n\n // Register a new pending promise to ensure that Behat waits for the editor setup to complete before continuing.\n const pendingPromise = new Pending('editor_tiny/editor:setupForTarget');\n\n // Get the list of plugins.\n const plugins = getPlugins(options);\n\n // Fetch the tinyMCE API, and instantiate the plugins.\n const [tinyMCE, pluginValues] = await Promise.all([\n getTinyMCE(),\n importPluginList(Object.keys(plugins)),\n ]);\n\n // TinyMCE uses the element ID as a map key internally, even if the target has changed.\n // In the case where we have an editor in a modal form which has been detached from the DOM, but the editor not removed,\n // we need to manually destroy the editor.\n // We could theoretically do this with a Mutation Observer, but in some cases the Node may be moved,\n // or added back elsewhere in the DOM.\n\n // First remove any detached editors.\n tinyMCE.get().filter((editor) => !editor.getElement().isConnected).forEach((editor) => {\n editor.remove();\n });\n\n // Now check for any existing editor which shares the same ID.\n const existingEditor = tinyMCE.EditorManager.get(target.id);\n if (existingEditor) {\n if (existingEditor.getElement() === target) {\n pendingPromise.resolve();\n return Promise.resolve(existingEditor);\n } else {\n pendingPromise.resolve();\n throw new Error('TinyMCE instance already exists for different target with same ID');\n }\n }\n\n // Get the editor configuration for this editor.\n const instanceConfig = getEditorConfiguration(target, tinyMCE, options, pluginValues);\n\n // Initialise the editor instance for the given configuration.\n // At this point any plugin which has configuration options registered will have them applied for this instance.\n const [editor] = await tinyMCE.init(instanceConfig);\n\n // Update the textarea when the editor to set the field type for Behat.\n target.dataset.fieldtype = 'editor';\n\n // Store the editor instance in the instanceMap and register a listener on removal to remove it from the map.\n instanceMap.set(target, editor);\n editor.on('remove', ({target}) => {\n // Handle removal of the editor from the map on destruction.\n instanceMap.delete(target.targetElm);\n target.targetElm.dataset.fieldtype = null;\n });\n\n // If the editor is part of a form, also listen to the jQuery submit event.\n // The jQuery submit event will not trigger the native submit event, and therefore the content will not be saved.\n // We cannot rely on listening to the bubbled submit event on the document because other events on child nodes may\n // consume the data before it is saved.\n if (target.form) {\n jQuery(target.form).on('submit', () => {\n editor.save();\n });\n }\n\n // Save the editor content to the textarea when the editor is blurred.\n editor.on('blur', () => {\n editor.save();\n });\n\n // If the editor is in a modal, we need to hide the modal when window editor's window is opened.\n editor.on('OpenWindow', () => {\n const modals = document.querySelectorAll('[data-region=\"modal\"]');\n if (modals) {\n modals.forEach((modal) => {\n if (!modal.classList.contains('hide')) {\n modal.classList.add('hide');\n }\n });\n }\n });\n\n // If the editor's window is closed, we need to show the hidden modal back.\n editor.on('CloseWindow', () => {\n if (isModalMode(target)) {\n const modals = document.querySelectorAll('[data-region=\"modal\"]');\n if (modals) {\n modals.forEach((modal) => {\n if (modal.classList.contains('hide')) {\n modal.classList.remove('hide');\n }\n });\n }\n }\n });\n\n pendingPromise.resolve();\n return editor;\n};\n\n/**\n * Set the default editor configuration.\n *\n * This configuration is used when an editor is initialised without any configuration.\n *\n * @param {object} [options={}]\n */\nexport const configureDefaultEditor = (options = {}) => {\n defaultOptions = options;\n};\n"],"names":["instanceMap","Map","defaultOptions","importPluginList","async","pluginHandlers","Promise","all","pluginList","map","pluginPath","indexOf","resolve","pluginNames","pluginConfig","Array","isArray","filter","value","entries","elementId","getInstanceForElement","document","getElementById","element","instance","get","removed","delete","_ref","options","target","setTimeout","setupForTarget","lang","querySelector","tinyMCE","langData","language","fetch","M","cfg","wwwroot","langrev","then","response","json","addI18n","initialisePage","getPlugins","plugins","getStandardConfig","config","Object","assign","base_url","baseUrl","min_height","height","clientHeight","content_css","css","convert_urls","a11y_advanced_options","extended_valid_elements","xss_sanitization","quickbars_insert_toolbar","quickbars_selection_toolbar","rows","block_formats","skin","help_accessibility","promotion","branding","table_header_type","entity_encoding","ui_mode","browser_spellcheck","setup","editor","Options","register","on","contentWindow","this","iframeElement","expectedEditingAreaHeight","parseFloat","window","getComputedStyle","lineHeight","getContainer","style","adjustEditorSize","addEventListener","dispatchEvent","Event","toolbar","getEditorConfiguration","pluginValues","instanceConfig","menu","file","items","format","replace","replaceAll","configure","forEach","pluginInstanceOverride","getInitialPluginConfiguration","isModalMode","closest","pendingPromise","Pending","keys","getElement","isConnected","remove","existingEditor","EditorManager","id","Error","init","dataset","fieldtype","set","_ref2","targetElm","form","save","modals","querySelectorAll","modal","classList","contains","add"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"editor.min.js","sources":["../src/editor.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * TinyMCE Editor Manager.\n *\n * @module editor_tiny/editor\n * @copyright 2022 Andrew Lyons \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport jQuery from 'jquery';\nimport Pending from 'core/pending';\nimport {getDefaultConfiguration, getDefaultQuickbarsSelectionToolbar} from './defaults';\nimport {getTinyMCE, baseUrl} from './loader';\nimport * as Options from './options';\nimport {addToolbarButton, addToolbarButtons, addToolbarSection,\n removeToolbarButton, removeSubmenuItem, updateEditorState} from './utils';\n\n/**\n * Storage for the TinyMCE instances on the page.\n * @type {Map}\n */\nconst instanceMap = new Map();\n\n/**\n * The default editor configuration.\n * @type {Object}\n */\nlet defaultOptions = {};\n\n/**\n * Require the modules for the named set of TinyMCE plugins.\n *\n * @param {string[]} pluginList The list of plugins\n * @return {Promise[]} A matching set of Promises relating to the requested plugins\n */\nconst importPluginList = async(pluginList) => {\n // Fetch all of the plugins from the list of plugins.\n // If a plugin contains a '/' then it is assumed to be a Moodle AMD module to import.\n const pluginHandlers = await Promise.all(pluginList.map(pluginPath => {\n if (pluginPath.indexOf('/') === -1) {\n // A standard TinyMCE Plugin.\n return Promise.resolve(pluginPath);\n }\n\n return import(pluginPath);\n }));\n\n // Normalise the plugin data to a list of plugin names.\n // Two formats are supported:\n // - a string; and\n // - an array whose first element is the plugin name, and the second element is the plugin configuration.\n const pluginNames = pluginHandlers.map((pluginConfig) => {\n if (typeof pluginConfig === 'string') {\n return pluginConfig;\n }\n if (Array.isArray(pluginConfig)) {\n return pluginConfig[0];\n }\n return null;\n }).filter((value) => value);\n\n // Fetch the list of pluginConfig handlers.\n const pluginConfig = pluginHandlers.map((pluginConfig) => {\n if (Array.isArray(pluginConfig)) {\n return pluginConfig[1];\n }\n return null;\n }).filter((value) => value);\n\n return {\n pluginNames,\n pluginConfig,\n };\n};\n\n/**\n * Fetch the language data for the specified language.\n *\n * @param {string} language The language identifier\n * @returns {object}\n */\nconst fetchLanguage = (language) => fetch(\n `${M.cfg.wwwroot}/lib/editor/tiny/lang.php/${M.cfg.langrev}/${language}`\n).then(response => response.json());\n\n/**\n * Get a list of all Editors in a Map, keyed by the DOM Node that the Editor is associated with.\n *\n * @returns {Map}\n */\nexport const getAllInstances = () => new Map(instanceMap.entries());\n\n/**\n * Get the TinyMCE instance for the specified Node ID.\n *\n * @param {string} elementId\n * @returns {TinyMCE|undefined}\n */\nexport const getInstanceForElementId = elementId => getInstanceForElement(document.getElementById(elementId));\n\n/*\n * Get the TinyMCE instance for the specified HTMLElement.\n *\n * @param {HTMLElement} element\n * @returns {TinyMCE|undefined}\n */\nexport const getInstanceForElement = element => {\n const instance = instanceMap.get(element);\n if (instance && instance.removed) {\n instanceMap.delete(element);\n return undefined;\n }\n return instance;\n};\n\n/**\n * Set up TinyMCE for the selector at the specified HTML Node id.\n *\n * @param {object} config The configuration required to setup the editor\n * @param {string} config.elementId The HTML Node ID\n * @param {Object} config.options The editor plugin configuration\n */\nexport const setupForElementId = ({elementId, options}) => {\n const target = document.getElementById(elementId);\n // We will need to wrap the setupForTarget and editor.remove() calls in a setTimeout.\n // Because other events callbacks will still try to run on the removed instance.\n // This will cause an error on Firefox.\n // We need to make TinyMCE to remove itself outside the event loop.\n // @see https://github.com/tinymce/tinymce/issues/3129 for more details.\n setTimeout(() => {\n return setupForTarget(target, options);\n }, 1);\n};\n\n/**\n * Initialise the page with standard TinyMCE requirements.\n *\n * Currently this includes the language taken from the HTML lang property.\n */\nconst initialisePage = async() => {\n const lang = document.querySelector('html').lang;\n\n const [tinyMCE, langData] = await Promise.all([getTinyMCE(), fetchLanguage(lang)]);\n tinyMCE.addI18n(lang, langData);\n};\ninitialisePage();\n\n/**\n * Get the list of plugins to load for the specified configuration.\n *\n * If the specified configuration does not include a plugin configuration, then return the default configuration.\n *\n * @param {object} options\n * @param {array} [options.plugins=null] The plugin list\n * @returns {object}\n */\nconst getPlugins = ({plugins = null} = {}) => {\n if (plugins) {\n return plugins;\n }\n\n if (defaultOptions.plugins) {\n return defaultOptions.plugins;\n }\n\n return {};\n};\n\n/**\n * Adjust the editor size base on the target element.\n *\n * @param {TinyMCE} editor TinyMCE editor\n * @param {Node} target Target element\n */\nconst adjustEditorSize = (editor, target) => {\n let expectedEditingAreaHeight = 0;\n if (target.clientHeight) {\n expectedEditingAreaHeight = target.clientHeight;\n } else {\n // If the target element is hidden, we cannot get the lineHeight of the target element.\n // 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.\n expectedEditingAreaHeight = target.rows * (parseFloat(window.getComputedStyle(target).lineHeight) || 22);\n }\n const currentEditingAreaHeight = editor.getContainer().querySelector('.tox-sidebar-wrap').clientHeight;\n if (currentEditingAreaHeight < expectedEditingAreaHeight) {\n // Change the height based on the target element's height.\n editor.getContainer().querySelector('.tox-sidebar-wrap').style.height = `${expectedEditingAreaHeight}px`;\n }\n};\n\n/**\n * Get the standard configuration for the specified options.\n *\n * @param {Node} target\n * @param {tinyMCE} tinyMCE\n * @param {object} options\n * @param {Array} plugins\n * @returns {object}\n */\nconst getStandardConfig = (target, tinyMCE, options, plugins) => {\n const lang = document.querySelector('html').lang;\n\n const config = Object.assign({}, getDefaultConfiguration(), {\n // eslint-disable-next-line camelcase\n base_url: baseUrl,\n\n // Set the editor target.\n // https://www.tiny.cloud/docs/tinymce/6/editor-important-options/#target\n target,\n\n // https://www.tiny.cloud/docs/tinymce/6/customize-ui/#set-maximum-and-minimum-heights-and-widths\n // Set the minimum height to the smallest height that we can fit the Menu bar, Tool bar, Status bar and the text area.\n // eslint-disable-next-line camelcase\n min_height: 175,\n\n // Base the height on the size of the text area.\n // In some cases, E.g.: The target is an advanced element, it will be hidden. We cannot get the height at this time.\n // So set the height to auto, and adjust it later by adjustEditorSize().\n height: target.clientHeight || 'auto',\n\n // Set the language.\n // https://www.tiny.cloud/docs/tinymce/6/ui-localization/#language\n // eslint-disable-next-line camelcase\n language: lang,\n\n // Load the editor stylesheet into the editor iframe.\n // https://www.tiny.cloud/docs/tinymce/6/add-css-options/\n // eslint-disable-next-line camelcase\n content_css: [\n options.css,\n ],\n\n // Do not convert URLs to relative URLs.\n // https://www.tiny.cloud/docs/tinymce/6/url-handling/#convert_urls\n // eslint-disable-next-line camelcase\n convert_urls: false,\n\n // Enabled 'advanced' a11y options.\n // This includes allowing role=\"presentation\" from the image uploader.\n // https://www.tiny.cloud/docs/tinymce/6/accessibility/\n // eslint-disable-next-line camelcase\n a11y_advanced_options: true,\n\n // Add specific rules to the valid elements.\n // eslint-disable-next-line camelcase\n extended_valid_elements: 'script[*],p[*],i[*]',\n\n // Disable XSS Sanitisation.\n // We do this in PHP.\n // https://www.tiny.cloud/docs/tinymce/6/security/#turning-dompurify-off\n // Note: This feature has been backported from TinyMCE 6.4.0.\n // eslint-disable-next-line camelcase\n xss_sanitization: false,\n\n // Disable quickbars entirely.\n // The UI is not ideal and we'll wait for it to improve in future before we enable it in Moodle.\n // eslint-disable-next-line camelcase\n quickbars_insert_toolbar: '',\n\n // If the target element is too small, disable the quickbars selection toolbar.\n // The quickbars selection toolbar is not displayed correctly if the target element is too small.\n // See: https://github.com/tinymce/tinymce/issues/9693.\n quickbars_selection_toolbar: target.rows > 5 ? getDefaultQuickbarsSelectionToolbar() : false,\n\n // Override the standard block formats property (removing h1 & h2).\n // https://www.tiny.cloud/docs/tinymce/6/user-formatting-options/#block_formats\n // eslint-disable-next-line camelcase\n block_formats: 'Paragraph=p;Heading 3=h3;Heading 4=h4;Heading 5=h5;Heading 6=h6;Preformatted=pre;Blockquote=blockquote',\n\n // The list of plugins to include in the instance.\n // https://www.tiny.cloud/docs/tinymce/6/editor-important-options/#plugins\n plugins: [\n ...plugins,\n ],\n\n // Skins\n skin: 'oxide',\n\n // Do not show the help link in the status bar.\n // https://www.tiny.cloud/docs/tinymce/latest/accessibility/#help_accessibility\n // eslint-disable-next-line camelcase\n help_accessibility: false,\n\n // Remove the \"Upgrade\" link for Tiny.\n // https://www.tiny.cloud/docs/tinymce/6/editor-premium-upgrade-promotion/\n promotion: false,\n\n // Allow the administrator to disable branding.\n // https://www.tiny.cloud/docs/tinymce/6/statusbar-configuration-options/#branding\n branding: options.branding,\n\n // Put th cells in a thead element.\n // https://www.tiny.cloud/docs/tinymce/6/table-options/#table_header_type\n // eslint-disable-next-line camelcase\n table_header_type: 'sectionCells',\n\n // Stored text in non-entity form.\n // https://www.tiny.cloud/docs/tinymce/6/content-filtering/#entity_encoding\n // eslint-disable-next-line camelcase\n entity_encoding: \"raw\",\n\n // Enable support for editors in scrollable containers.\n // https://www.tiny.cloud/docs/tinymce/6/ui-mode-configuration-options/#ui_mode\n // eslint-disable-next-line camelcase\n ui_mode: 'split',\n\n // Enable browser-supported spell checking.\n // https://www.tiny.cloud/docs/tinymce/latest/spelling/\n // eslint-disable-next-line camelcase\n browser_spellcheck: true,\n\n // Set the license_key to gpl.\n // https://www.tiny.cloud/docs/tinymce/latest/license-key/\n // eslint-disable-next-line camelcase\n license_key: 'gpl',\n\n setup: (editor) => {\n Options.register(editor, options);\n\n editor.on('PreInit', function() {\n // Work around a bug in TinyMCE with Firefox.\n // When an editor is removed, and replaced with an identically attributed editor (same ID),\n // and the Firefox window is freshly opened (e.g. Behat, Private browsing), the wrong contentWindow\n // is assigned to the editor instance leading to an NS_ERROR_UNEXPECTED error in Firefox.\n // This is a workaround for that issue.\n this.contentWindow = this.iframeElement.contentWindow;\n });\n editor.on('init', function() {\n // Hide justify alignment sub-menu.\n removeSubmenuItem(editor, 'align', 'tiny:justify');\n // Adjust the editor size.\n adjustEditorSize(editor, target);\n });\n\n target.addEventListener('form:editorUpdated', function() {\n updateEditorState(editor, target);\n });\n\n target.dispatchEvent(new Event('form:editorUpdated'));\n },\n });\n\n config.toolbar = addToolbarSection(config.toolbar, 'content', 'formatting', true);\n config.toolbar = addToolbarButton(config.toolbar, 'content', 'link');\n\n // Add directionality plugins, always.\n config.toolbar = addToolbarSection(config.toolbar, 'directionality', 'alignment', true);\n config.toolbar = addToolbarButtons(config.toolbar, 'directionality', ['ltr', 'rtl']);\n\n // Remove the align justify button from the toolbar.\n config.toolbar = removeToolbarButton(config.toolbar, 'alignment', 'alignjustify');\n\n return config;\n};\n\n/**\n * Fetch the TinyMCE configuration for this editor instance.\n *\n * @param {HTMLElement} target\n * @param {TinyMCE} tinyMCE The TinyMCE API\n * @param {Object} options The editor plugin configuration\n * @param {object} pluginValues\n * @param {object} pluginValues.pluginConfig The list of plugin configuration\n * @param {object} pluginValues.pluginNames The list of plugins to load\n * @returns {object} The TinyMCE Configuration\n */\nconst getEditorConfiguration = (target, tinyMCE, options, pluginValues) => {\n const {\n pluginNames,\n pluginConfig,\n } = pluginValues;\n\n // Allow plugins to modify the configuration.\n // This seems a little strange, but we must double-process the config slightly.\n\n // First we fetch the standard configuration.\n const instanceConfig = getStandardConfig(target, tinyMCE, options, pluginNames);\n\n // Next we make any standard changes.\n // Here we remove the file menu, as it doesn't offer any useful functionality.\n // We only empty the items list so that a plugin may choose to add to it themselves later if they wish.\n if (instanceConfig.menu.file) {\n instanceConfig.menu.file.items = '';\n }\n\n // We disable the styles, backcolor, and forecolor plugins from the format menu.\n // These are not useful for Moodle and we don't want to encourage their use.\n if (instanceConfig.menu.format) {\n instanceConfig.menu.format.items = instanceConfig.menu.format.items\n // Remove forecolor and backcolor.\n .replace(/forecolor ?/, '')\n .replace(/backcolor ?/, '')\n\n // Remove fontfamily for now.\n .replace(/fontfamily ?/, '')\n\n // Remove fontsize for now.\n .replace(/fontsize ?/, '')\n\n // Remove styles - it just duplicates the format menu in a way which does not respect configuration\n .replace(/styles ?/, '')\n\n // Remove any duplicate separators.\n .replaceAll(/\\| *\\|/g, '|');\n }\n\n if (instanceConfig.quickbars_selection_toolbar !== false) {\n // eslint-disable-next-line camelcase\n instanceConfig.quickbars_selection_toolbar = instanceConfig.quickbars_selection_toolbar.replace('h2 h3', 'h3 h4 h5 h6');\n }\n\n // Next we call the `configure` function for any plugin which defines it.\n // We pass the current instanceConfig in here, to allow them to make certain changes to the global configuration.\n // For example, to add themselves to any menu, toolbar, and so on.\n // Any plugin which wishes to have configuration options must register those options here.\n pluginConfig.filter((pluginConfig) => typeof pluginConfig.configure === 'function').forEach((pluginConfig) => {\n const pluginInstanceOverride = pluginConfig.configure(instanceConfig, options);\n Object.assign(instanceConfig, pluginInstanceOverride);\n });\n\n // Next we convert the plugin configuration into a format that TinyMCE understands.\n Object.assign(instanceConfig, Options.getInitialPluginConfiguration(options));\n\n return instanceConfig;\n};\n\n/**\n * Check if the target for TinyMCE is in a modal or not.\n *\n * @param {HTMLElement} target Target to check\n * @returns {boolean} True if the target is in a modal form.\n */\nconst isModalMode = (target) => {\n return !!target.closest('[data-region=\"modal\"]');\n};\n\n/**\n * Set up TinyMCE for the HTML Element.\n *\n * @param {HTMLElement} target\n * @param {Object} [options={}] The editor plugin configuration\n * @return {Promise} The TinyMCE instance\n */\nexport const setupForTarget = async(target, options = {}) => {\n const instance = getInstanceForElement(target);\n if (instance) {\n return Promise.resolve(instance);\n }\n\n // Register a new pending promise to ensure that Behat waits for the editor setup to complete before continuing.\n const pendingPromise = new Pending('editor_tiny/editor:setupForTarget');\n\n // Get the list of plugins.\n const plugins = getPlugins(options);\n\n // Fetch the tinyMCE API, and instantiate the plugins.\n const [tinyMCE, pluginValues] = await Promise.all([\n getTinyMCE(),\n importPluginList(Object.keys(plugins)),\n ]);\n\n // TinyMCE uses the element ID as a map key internally, even if the target has changed.\n // In the case where we have an editor in a modal form which has been detached from the DOM, but the editor not removed,\n // we need to manually destroy the editor.\n // We could theoretically do this with a Mutation Observer, but in some cases the Node may be moved,\n // or added back elsewhere in the DOM.\n\n // First remove any detached editors.\n tinyMCE.get().filter((editor) => !editor.getElement().isConnected).forEach((editor) => {\n editor.remove();\n });\n\n // Now check for any existing editor which shares the same ID.\n const existingEditor = tinyMCE.EditorManager.get(target.id);\n if (existingEditor) {\n if (existingEditor.getElement() === target) {\n pendingPromise.resolve();\n return Promise.resolve(existingEditor);\n } else {\n pendingPromise.resolve();\n throw new Error('TinyMCE instance already exists for different target with same ID');\n }\n }\n\n // Get the editor configuration for this editor.\n const instanceConfig = getEditorConfiguration(target, tinyMCE, options, pluginValues);\n\n // Initialise the editor instance for the given configuration.\n // At this point any plugin which has configuration options registered will have them applied for this instance.\n const [editor] = await tinyMCE.init(instanceConfig);\n\n // Update the textarea when the editor to set the field type for Behat.\n target.dataset.fieldtype = 'editor';\n\n // Store the editor instance in the instanceMap and register a listener on removal to remove it from the map.\n instanceMap.set(target, editor);\n editor.on('remove', ({target}) => {\n // Handle removal of the editor from the map on destruction.\n instanceMap.delete(target.targetElm);\n target.targetElm.dataset.fieldtype = null;\n });\n\n // If the editor is part of a form, also listen to the jQuery submit event.\n // The jQuery submit event will not trigger the native submit event, and therefore the content will not be saved.\n // We cannot rely on listening to the bubbled submit event on the document because other events on child nodes may\n // consume the data before it is saved.\n if (target.form) {\n jQuery(target.form).on('submit', () => {\n editor.save();\n });\n }\n\n // Save the editor content to the textarea when the editor is blurred.\n editor.on('blur', () => {\n editor.save();\n });\n\n // If the editor is in a modal, we need to hide the modal when window editor's window is opened.\n editor.on('OpenWindow', () => {\n const modals = document.querySelectorAll('[data-region=\"modal\"]');\n if (modals) {\n modals.forEach((modal) => {\n if (!modal.classList.contains('hide')) {\n modal.classList.add('hide');\n }\n });\n }\n });\n\n // If the editor's window is closed, we need to show the hidden modal back.\n editor.on('CloseWindow', () => {\n if (isModalMode(target)) {\n const modals = document.querySelectorAll('[data-region=\"modal\"]');\n if (modals) {\n modals.forEach((modal) => {\n if (modal.classList.contains('hide')) {\n modal.classList.remove('hide');\n }\n });\n }\n }\n });\n\n pendingPromise.resolve();\n return editor;\n};\n\n/**\n * Set the default editor configuration.\n *\n * This configuration is used when an editor is initialised without any configuration.\n *\n * @param {object} [options={}]\n */\nexport const configureDefaultEditor = (options = {}) => {\n defaultOptions = options;\n};\n"],"names":["instanceMap","Map","defaultOptions","importPluginList","async","pluginHandlers","Promise","all","pluginList","map","pluginPath","indexOf","resolve","pluginNames","pluginConfig","Array","isArray","filter","value","entries","elementId","getInstanceForElement","document","getElementById","element","instance","get","removed","delete","_ref","options","target","setTimeout","setupForTarget","lang","querySelector","tinyMCE","langData","language","fetch","M","cfg","wwwroot","langrev","then","response","json","addI18n","initialisePage","getPlugins","plugins","getStandardConfig","config","Object","assign","base_url","baseUrl","min_height","height","clientHeight","content_css","css","convert_urls","a11y_advanced_options","extended_valid_elements","xss_sanitization","quickbars_insert_toolbar","quickbars_selection_toolbar","rows","block_formats","skin","help_accessibility","promotion","branding","table_header_type","entity_encoding","ui_mode","browser_spellcheck","license_key","setup","editor","Options","register","on","contentWindow","this","iframeElement","expectedEditingAreaHeight","parseFloat","window","getComputedStyle","lineHeight","getContainer","style","adjustEditorSize","addEventListener","dispatchEvent","Event","toolbar","getEditorConfiguration","pluginValues","instanceConfig","menu","file","items","format","replace","replaceAll","configure","forEach","pluginInstanceOverride","getInitialPluginConfiguration","isModalMode","closest","pendingPromise","Pending","keys","getElement","isConnected","remove","existingEditor","EditorManager","id","Error","init","dataset","fieldtype","set","_ref2","targetElm","form","save","modals","querySelectorAll","modal","classList","contains","add"],"mappings":"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"} \ No newline at end of file diff --git a/lib/editor/tiny/amd/src/editor.js b/lib/editor/tiny/amd/src/editor.js index 029f79dadad..bad58e20f58 100644 --- a/lib/editor/tiny/amd/src/editor.js +++ b/lib/editor/tiny/amd/src/editor.js @@ -323,6 +323,11 @@ const getStandardConfig = (target, tinyMCE, options, plugins) => { // eslint-disable-next-line camelcase browser_spellcheck: true, + // Set the license_key to gpl. + // https://www.tiny.cloud/docs/tinymce/latest/license-key/ + // eslint-disable-next-line camelcase + license_key: 'gpl', + setup: (editor) => { Options.register(editor, options); diff --git a/lib/editor/tiny/classes/manager.php b/lib/editor/tiny/classes/manager.php index b6d16ced508..61a483b133a 100644 --- a/lib/editor/tiny/classes/manager.php +++ b/lib/editor/tiny/classes/manager.php @@ -440,14 +440,6 @@ class manager { 'tabledeleterow' => 'table', ], ], - 'template' => [ - 'buttons' => [ - 'template', - ], - 'menuitems' => [ - 'template' => 'insert', - ], - ], 'visualblocks' => [ 'buttons' => [ 'visualblocks', @@ -492,9 +484,6 @@ class manager { // Use the Moodle autosave plugin instead. 'autosave', - // Disable the Template plugin for now. - 'template', - // Disable the preview plugin as it does not support Moodle filters. 'preview', diff --git a/lib/editor/tiny/readme_moodle.md b/lib/editor/tiny/readme_moodle.md index ebe4498791d..dc53fe8950d 100644 --- a/lib/editor/tiny/readme_moodle.md +++ b/lib/editor/tiny/readme_moodle.md @@ -1,16 +1,13 @@ -# This is a description for the TinyMCE 6 library integration with Moodle. +# This is a description for the TinyMCE 7 library integration with Moodle. Please note that we have a clone of the official TinyMCE repository which contains the working build and branch for each release. This ensures build repeatability and gives us the ability to patch stable versions of Moodle for security fixes where relevant. Each Moodle branch has a similar branch in the https://github.com/moodlehq/tinymce. -The Moodle `master` branch is named as the upcoming STABLE branch name, for example during the development of Moodle 4.2.0, the upcoming STABLE branch name will be MOODLE_402_STABLE. +The Moodle `master` branch is named as the upcoming STABLE branch name, for example during the development of Moodle 4.5.0, the upcoming STABLE branch name will be MOODLE_405_STABLE. ## Patches included in this release -- MDL-78714: Add support for disabling XSS Sanitisation (TINY-9600) - -Please note: TinyMCE issue numbers are related to bugs in their private issue -tracker. See git history of their repository for relevant information. +N/A ## Upgrade procedure for TinyMCE Editor @@ -27,7 +24,7 @@ tinymce=`mktemp -d` cd "${tinymce}" git clone https://github.com/tinymce/tinymce.git cd tinymce -git checkout -b MOODLE_404_STABLE +git checkout -b MOODLE_405_STABLE git reset --hard [desired version] ``` @@ -62,16 +59,16 @@ cp -r modules/tinymce/js "${MOODLEDIR}/js" ``` # Tag the next Moodle version. -git tag v4.2.0 +git tag v4.5.0 git remote add moodlehq --tags -git push moodlehq MOODLE_402_STABLE +git push moodlehq MOODLE_405_STABLE ``` -9. Check the (Release notes)[https://www.tiny.cloud/docs/tinymce/6/release-notes/] for any new plugins, premium plugins, menu items, or buttons and add them to classes/manager.php +9. Check the (Release notes)[https://www.tiny.cloud/docs/tinymce/7/release-notes/] for any new plugins, premium plugins, menu items, or buttons and add them to classes/manager.php ## Update procedure for included TinyMCE translations -1. Visit https://www.tiny.cloud/get-tiny/language-packages/ and download the "TinyMCE 6 All languages" zip file. +1. Visit https://www.tiny.cloud/get-tiny/language-packages/ and download the "TinyMCE 7 All languages" zip file. 2. Check the list of languages and confirm that the German translation is still at 100%. If not, then make a note of a language which is. 3. Unzip the translation into a new directory: @@ -130,7 +127,7 @@ git clone https://github.com/tinymce/tinymce.git cd tinymce git remote add moodlehq https://github.com/moodlehq/tinymce git fetch moodlehq -git checkout -b MOODLE_402_STABLE moodlehq/MOODLE_402_STABLE +git checkout -b MOODLE_405_STABLE moodlehq/MOODLE_405_STABLE ``` 3. Apply any necessary security patches. diff --git a/lib/editor/tiny/tests/behat/behat_editor_tiny.php b/lib/editor/tiny/tests/behat/behat_editor_tiny.php index b9456df5b15..90e50f64f41 100644 --- a/lib/editor/tiny/tests/behat/behat_editor_tiny.php +++ b/lib/editor/tiny/tests/behat/behat_editor_tiny.php @@ -170,7 +170,7 @@ class behat_editor_tiny extends behat_base implements \core_behat\settable_edito $firstlink->mouseover(); // Now match by title where the role matches any menuitem, or menuitemcheckbox, or menuitem*. - $link = $openmenu->find('css', "[title='{$menuitem}'][role^='menuitem']"); + $link = $openmenu->find('css', "[aria-label='{$menuitem}'][role^='menuitem']"); $this->execute('behat_general::i_click_on', [$link, 'NodeElement']); } } @@ -301,7 +301,7 @@ class behat_editor_tiny extends behat_base implements \core_behat\settable_edito return; } - if ($button->getAttribute(('aria-pressed')) === 'false') { + if ($button->getAttribute(('aria-expanded')) === 'false') { $this->execute('behat_general::i_click_on', [$button, 'NodeElement']); } }