This commit is contained in:
Markos Gogoulos
2026-02-03 19:23:02 +02:00
parent c4d569e7b0
commit e12f361935
89 changed files with 8689 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
define("tiny_mediacms/autoconvert",["exports","./options"],(function(_exports,_options){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.setupAutoConvert=_exports.isMediaCMSUrl=_exports.convertToEmbed=void 0;
/**
* Tiny MediaCMS Auto-convert module.
*
* This module automatically converts pasted MediaCMS URLs into embedded videos.
* When a user pastes a MediaCMS video URL (e.g., https://deic.mediacms.io/view?m=JpBd1Zvdl),
* it will be automatically converted to an iframe embed.
*
* @module tiny_mediacms/autoconvert
* @copyright 2024
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const MEDIACMS_VIEW_URL_PATTERN=/^(https?:\/\/[^\/]+)\/view\?m=([a-zA-Z0-9_-]+)$/,parseMediaCMSUrl=text=>{if(!text||"string"!=typeof text)return null;const trimmed=text.trim(),match=trimmed.match(MEDIACMS_VIEW_URL_PATTERN);return match?{baseUrl:match[1],videoId:match[2],originalUrl:trimmed}:null},isDomainAllowed=(parsed,config)=>{const configuredBaseUrl=config.autoConvertBaseUrl||config.mediacmsBaseUrl;if(!configuredBaseUrl)return!0;try{const configuredUrl=new URL(configuredBaseUrl),pastedUrl=new URL(parsed.baseUrl);return configuredUrl.host===pastedUrl.host}catch(e){return!0}},generateEmbedHtml=function(parsed){let options=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const embedUrl=new URL("".concat(parsed.baseUrl,"/embed"));embedUrl.searchParams.set("m",parsed.videoId),embedUrl.searchParams.set("showTitle",!1!==options.showTitle?"1":"0"),embedUrl.searchParams.set("showRelated",!1!==options.showRelated?"1":"0"),embedUrl.searchParams.set("showUserAvatar",!1!==options.showUserAvatar?"1":"0"),embedUrl.searchParams.set("linkTitle",!1!==options.linkTitle?"1":"0");const html='<iframe src="'.concat(embedUrl.toString(),'" ')+'style="width: 100%; aspect-ratio: 16 / 9; display: block; border: 0;" allowfullscreen="allowfullscreen"></iframe>';return html};_exports.setupAutoConvert=editor=>{const config=(0,_options.getData)(editor)||{};!1!==config.autoConvertEnabled&&(editor.on("paste",(e=>{handlePasteEvent(editor,e,config)})),editor.on("input",(e=>{handleInputEvent(editor,e,config)})))};const handlePasteEvent=(editor,e,config)=>{const clipboardData=e.clipboardData||window.clipboardData;if(!clipboardData)return;const text=clipboardData.getData("text/plain")||clipboardData.getData("text");if(!text)return;const parsed=parseMediaCMSUrl(text);if(!parsed)return;if(!isDomainAllowed(parsed,config))return;e.preventDefault(),e.stopPropagation();const embedHtml=generateEmbedHtml(parsed,config.autoConvertOptions||{});setTimeout((()=>{editor.insertContent(embedHtml),editor.selection.collapse(!1)}),0)},handleInputEvent=(editor,e,config)=>{if("insertFromPaste"!==e.inputType&&"insertText"!==e.inputType)return;const node=editor.selection.getNode();if(!node||"P"!==node.nodeName)return;const text=node.textContent||"",parsed=parseMediaCMSUrl(text);if(!parsed||!isDomainAllowed(parsed,config))return;const trimmedHtml=node.innerHTML.trim();if(trimmedHtml!==text.trim()&&!trimmedHtml.startsWith(text.trim()))return;const embedHtml=generateEmbedHtml(parsed,config.autoConvertOptions||{});setTimeout((()=>{const currentText=node.textContent||"",currentParsed=parseMediaCMSUrl(currentText);currentParsed&&currentParsed.originalUrl===parsed.originalUrl&&(editor.selection.select(node),editor.insertContent(embedHtml))}),100)};_exports.isMediaCMSUrl=text=>null!==parseMediaCMSUrl(text);_exports.convertToEmbed=function(url){let options=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};const parsed=parseMediaCMSUrl(url);return parsed?generateEmbedHtml(parsed,options):null}}));
//# sourceMappingURL=autoconvert.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

View File

@@ -0,0 +1,3 @@
define("tiny_mediacms/common",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={pluginName:"tiny_mediacms/plugin",component:"tiny_mediacms",iframeButtonName:"tiny_mediacms_iframe",iframeMenuItemName:"tiny_mediacms_iframe",iframeIcon:"tiny_mediacms_iframe"},_exports.default}));
//# sourceMappingURL=common.min.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"common.min.js","sources":["../src/common.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Media common values.\n *\n * @module tiny_mediacms/common\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n pluginName: 'tiny_mediacms/plugin',\n component: 'tiny_mediacms',\n iframeButtonName: 'tiny_mediacms_iframe',\n iframeMenuItemName: 'tiny_mediacms_iframe',\n iframeIcon: 'tiny_mediacms_iframe',\n};\n"],"names":["pluginName","component","iframeButtonName","iframeMenuItemName","iframeIcon"],"mappings":"sKAuBe,CACXA,WAAY,uBACZC,UAAW,gBACXC,iBAAkB,uBAClBC,mBAAoB,uBACpBC,WAAY"}

View File

@@ -0,0 +1,3 @@
define("tiny_mediacms/configuration",["exports","./common","editor_tiny/utils"],(function(_exports,_common,_utils){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.configure=void 0;_exports.configure=instanceConfig=>{return{contextmenu:(0,_utils.addContextmenuItem)(instanceConfig.contextmenu,_common.iframeButtonName),menu:(menu=instanceConfig.menu,menu.insert.items="".concat(_common.iframeMenuItemName," ").concat(menu.insert.items),menu),toolbar:(toolbar=instanceConfig.toolbar,toolbar.map((section=>("content"===section.name&&section.items.unshift(_common.iframeButtonName),section))))};var toolbar,menu}}));
//# sourceMappingURL=configuration.min.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"configuration.min.js","sources":["../src/configuration.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Media configuration.\n *\n * @module tiny_mediacms/configuration\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {\n iframeButtonName,\n iframeMenuItemName,\n} from './common';\nimport {\n addContextmenuItem,\n} from 'editor_tiny/utils';\n\nconst configureMenu = (menu) => {\n // Add the Iframe Embed to the insert menu.\n menu.insert.items = `${iframeMenuItemName} ${menu.insert.items}`;\n\n return menu;\n};\n\nconst configureToolbar = (toolbar) => {\n // The toolbar contains an array of named sections.\n // The Moodle integration ensures that there is a section called 'content'.\n\n return toolbar.map((section) => {\n if (section.name === 'content') {\n // Insert the iframe button at the start of it.\n section.items.unshift(iframeButtonName);\n }\n\n return section;\n });\n};\n\nexport const configure = (instanceConfig) => {\n // Update the instance configuration to add the Iframe Embed menu option to the menus and toolbars.\n return {\n contextmenu: addContextmenuItem(instanceConfig.contextmenu, iframeButtonName),\n menu: configureMenu(instanceConfig.menu),\n toolbar: configureToolbar(instanceConfig.toolbar),\n };\n};\n"],"names":["instanceConfig","contextmenu","iframeButtonName","menu","insert","items","iframeMenuItemName","toolbar","map","section","name","unshift"],"mappings":"wNAoD0BA,uBAEf,CACHC,aAAa,6BAAmBD,eAAeC,YAAaC,0BAC5DC,MAzBeA,KAyBKH,eAAeG,KAvBvCA,KAAKC,OAAOC,gBAAWC,uCAAsBH,KAAKC,OAAOC,OAElDF,MAsBHI,SAnBkBA,QAmBQP,eAAeO,QAftCA,QAAQC,KAAKC,UACK,YAAjBA,QAAQC,MAERD,QAAQJ,MAAMM,QAAQT,0BAGnBO,aAVWF,IAAAA,QAPHJ"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
define("tiny_mediacms/embedmodal",["exports","core/modal","./common"],(function(_exports,_modal,_common){var obj;function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=(obj=_modal)&&obj.__esModule?obj:{default:obj};class EmbedModal extends _modal.default{registerEventListeners(){super.registerEventListeners(),this.registerCloseOnSave(),this.registerCloseOnCancel()}configure(modalConfig){modalConfig.large=!0,modalConfig.removeOnClose=!0,modalConfig.show=!0,super.configure(modalConfig)}}return _exports.default=EmbedModal,_defineProperty(EmbedModal,"TYPE","".concat(_common.component,"/modal")),_defineProperty(EmbedModal,"TEMPLATE","".concat(_common.component,"/embed_media_modal")),_exports.default}));
//# sourceMappingURL=embedmodal.min.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"embedmodal.min.js","sources":["../src/embedmodal.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 * Embedded Media Management Modal for Tiny.\n *\n * @module tiny_mediacms/embedmodal\n * @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Modal from 'core/modal';\nimport {component} from './common';\n\nexport default class EmbedModal extends Modal {\n static TYPE = `${component}/modal`;\n static TEMPLATE = `${component}/embed_media_modal`;\n\n registerEventListeners() {\n // Call the parent registration.\n super.registerEventListeners();\n\n // Register to close on save/cancel.\n this.registerCloseOnSave();\n this.registerCloseOnCancel();\n }\n\n configure(modalConfig) {\n modalConfig.large = true;\n modalConfig.removeOnClose = true;\n modalConfig.show = true;\n\n super.configure(modalConfig);\n }\n}\n"],"names":["EmbedModal","Modal","registerEventListeners","registerCloseOnSave","registerCloseOnCancel","configure","modalConfig","large","removeOnClose","show","component"],"mappings":"iaA0BqBA,mBAAmBC,eAIpCC,+BAEUA,8BAGDC,2BACAC,wBAGTC,UAAUC,aACNA,YAAYC,OAAQ,EACpBD,YAAYE,eAAgB,EAC5BF,YAAYG,MAAO,QAEbJ,UAAUC,iEAlBHN,4BACAU,6CADAV,gCAEIU"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
define("tiny_mediacms/iframemodal",["exports","core/modal","./common"],(function(_exports,_modal,_common){var obj;function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=(obj=_modal)&&obj.__esModule?obj:{default:obj};class IframeModal extends _modal.default{registerEventListeners(){super.registerEventListeners(),this.registerCloseOnSave(),this.registerCloseOnCancel()}configure(modalConfig){modalConfig.large=!0,modalConfig.removeOnClose=!0,modalConfig.show=!0,super.configure(modalConfig)}}return _exports.default=IframeModal,_defineProperty(IframeModal,"TYPE","".concat(_common.component,"/iframemodal")),_defineProperty(IframeModal,"TEMPLATE","".concat(_common.component,"/iframe_embed_modal")),_exports.default}));
//# sourceMappingURL=iframemodal.min.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"iframemodal.min.js","sources":["../src/iframemodal.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 * Iframe Embed Modal for Tiny Media2.\n *\n * @module tiny_mediacms/iframemodal\n * @copyright 2024\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Modal from 'core/modal';\nimport {component} from './common';\n\nexport default class IframeModal extends Modal {\n static TYPE = `${component}/iframemodal`;\n static TEMPLATE = `${component}/iframe_embed_modal`;\n\n registerEventListeners() {\n // Call the parent registration.\n super.registerEventListeners();\n\n // Register to close on save/cancel.\n this.registerCloseOnSave();\n this.registerCloseOnCancel();\n }\n\n configure(modalConfig) {\n modalConfig.large = true;\n modalConfig.removeOnClose = true;\n modalConfig.show = true;\n\n super.configure(modalConfig);\n }\n}\n"],"names":["IframeModal","Modal","registerEventListeners","registerCloseOnSave","registerCloseOnCancel","configure","modalConfig","large","removeOnClose","show","component"],"mappings":"kaA0BqBA,oBAAoBC,eAIrCC,+BAEUA,8BAGDC,2BACAC,wBAGTC,UAAUC,aACNA,YAAYC,OAAQ,EACpBD,YAAYE,eAAgB,EAC5BF,YAAYG,MAAO,QAEbJ,UAAUC,kEAlBHN,6BACAU,mDADAV,iCAEIU"}

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

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,10 @@
define("tiny_mediacms/imagehelpers",["exports","core/templates"],(function(_exports,_templates){var obj;
/**
* Tiny media plugin image helpers.
*
* @module tiny_mediacms/imagehelpers
* @copyright 2024 Meirza <meirza.arson@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.showElements=_exports.isPercentageValue=_exports.hideElements=_exports.footerImageInsert=_exports.footerImageDetails=_exports.bodyImageInsert=_exports.bodyImageDetails=void 0,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj};_exports.bodyImageInsert=async(templateContext,root)=>_templates.default.renderForPromise("tiny_mediacms/insert_image_modal_insert",{...templateContext}).then((_ref=>{let{html:html,js:js}=_ref;_templates.default.replaceNodeContents(root.querySelector(".tiny_imagecms_body_template"),html,js)})).catch((error=>{window.console.log(error)}));_exports.footerImageInsert=async(templateContext,root)=>_templates.default.renderForPromise("tiny_mediacms/insert_image_modal_insert_footer",{...templateContext}).then((_ref2=>{let{html:html,js:js}=_ref2;_templates.default.replaceNodeContents(root.querySelector(".tiny_imagecms_footer_template"),html,js)})).catch((error=>{window.console.log(error)}));_exports.bodyImageDetails=async(templateContext,root)=>_templates.default.renderForPromise("tiny_mediacms/insert_image_modal_details",{...templateContext}).then((_ref3=>{let{html:html,js:js}=_ref3;_templates.default.replaceNodeContents(root.querySelector(".tiny_imagecms_body_template"),html,js)})).catch((error=>{window.console.log(error)}));_exports.footerImageDetails=async(templateContext,root)=>_templates.default.renderForPromise("tiny_mediacms/insert_image_modal_details_footer",{...templateContext}).then((_ref4=>{let{html:html,js:js}=_ref4;_templates.default.replaceNodeContents(root.querySelector(".tiny_imagecms_footer_template"),html,js)})).catch((error=>{window.console.log(error)}));_exports.showElements=(elements,root)=>{if(elements instanceof Array)elements.forEach((elementSelector=>{const element=root.querySelector(elementSelector);element&&element.classList.remove("d-none")}));else{const element=root.querySelector(elements);element&&element.classList.remove("d-none")}};_exports.hideElements=(elements,root)=>{if(elements instanceof Array)elements.forEach((elementSelector=>{const element=root.querySelector(elementSelector);element&&element.classList.add("d-none")}));else{const element=root.querySelector(elements);element&&element.classList.add("d-none")}};_exports.isPercentageValue=value=>value.match(/\d+%/)}));
//# sourceMappingURL=imagehelpers.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

View File

@@ -0,0 +1,3 @@
define("tiny_mediacms/imagemodal",["exports","core/modal","./common"],(function(_exports,_modal,_common){var obj;function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_modal=(obj=_modal)&&obj.__esModule?obj:{default:obj};class ImageModal extends _modal.default{registerEventListeners(){super.registerEventListeners(),this.registerCloseOnSave(),this.registerCloseOnCancel()}configure(modalConfig){modalConfig.large=!0,modalConfig.removeOnClose=!0,modalConfig.show=!0,super.configure(modalConfig)}}return _exports.default=ImageModal,_defineProperty(ImageModal,"TYPE","".concat(_common.component,"/imagemodal")),_defineProperty(ImageModal,"TEMPLATE","".concat(_common.component,"/insert_image_modal")),ImageModal.registerModalType(),_exports.default}));
//# sourceMappingURL=imagemodal.min.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"imagemodal.min.js","sources":["../src/imagemodal.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 * Image Modal for Tiny.\n *\n * @module tiny_mediacms/imagemodal\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Modal from 'core/modal';\nimport {component} from './common';\n\nexport default class ImageModal extends Modal {\n static TYPE = `${component}/imagemodal`;\n static TEMPLATE = `${component}/insert_image_modal`;\n\n registerEventListeners() {\n // Call the parent registration.\n super.registerEventListeners();\n\n // Register to close on save/cancel.\n this.registerCloseOnSave();\n this.registerCloseOnCancel();\n }\n\n configure(modalConfig) {\n modalConfig.large = true;\n modalConfig.removeOnClose = true;\n modalConfig.show = true;\n\n super.configure(modalConfig);\n }\n}\n\nImageModal.registerModalType();\n"],"names":["ImageModal","Modal","registerEventListeners","registerCloseOnSave","registerCloseOnCancel","configure","modalConfig","large","removeOnClose","show","component","registerModalType"],"mappings":"iaA0BqBA,mBAAmBC,eAIpCC,+BAEUA,8BAGDC,2BACAC,wBAGTC,UAAUC,aACNA,YAAYC,OAAQ,EACpBD,YAAYE,eAAgB,EAC5BF,YAAYG,MAAO,QAEbJ,UAAUC,iEAlBHN,4BACAU,kDADAV,gCAEIU,0CAoBzBV,WAAWW"}

View File

@@ -0,0 +1,3 @@
define("tiny_mediacms/manager",["exports","core/templates","core/str","core/modal","core/modal_events","./options","core/config"],(function(_exports,_templates,_str,_modal,ModalEvents,_options,_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)}function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_templates=_interopRequireDefault(_templates),_modal=_interopRequireDefault(_modal),ModalEvents=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}(ModalEvents),_config=_interopRequireDefault(_config);return _exports.default=class{constructor(editor){_defineProperty(this,"editor",null),_defineProperty(this,"area",null),this.editor=editor;const data=(0,_options.getData)(editor);this.area=data.params.area,this.area.itemid=data.fpoptions.image.itemid}async displayDialogue(){const modal=await _modal.default.create({large:!0,title:(0,_str.getString)("mediamanagerproperties","tiny_mediacms"),body:_templates.default.render("tiny_mediacms/mm2_iframe",{src:this.getIframeURL()}),removeOnClose:!0,show:!0});return modal.getRoot().on(ModalEvents.bodyRendered,(()=>{this.selectFirstElement()})),document.querySelector(".modal-lg").style.cssText="max-width: 850px",modal}selectFirstElement(){const iframe=document.getElementById("mm2-iframe");iframe.addEventListener("load",(function(){let intervalId=setInterval((function(){const iDocument=iframe.contentWindow.document;if(iDocument.querySelector(".filemanager")){const firstFocusableElement=iDocument.querySelector(".fp-navbar a:not([disabled])");firstFocusableElement&&firstFocusableElement.focus(),clearInterval(intervalId)}}),200)}))}getIframeURL(){const url=new URL("".concat(_config.default.wwwroot,"/lib/editor/tiny/plugins/mediacms/manage.php"));url.searchParams.append("elementid",this.editor.getElement().id);for(const key in this.area)url.searchParams.append(key,this.area[key]);return url.toString()}},_exports.default}));
//# sourceMappingURL=manager.min.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"manager.min.js","sources":["../src/manager.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny Media Manager plugin class for Moodle.\n *\n * @module tiny_mediacms/manager\n * @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Templates from 'core/templates';\nimport {getString} from 'core/str';\nimport Modal from 'core/modal';\nimport * as ModalEvents from 'core/modal_events';\nimport {getData} from './options';\nimport Config from 'core/config';\n\nexport default class MediaManager {\n\n editor = null;\n area = null;\n\n constructor(editor) {\n this.editor = editor;\n const data = getData(editor);\n this.area = data.params.area;\n this.area.itemid = data.fpoptions.image.itemid;\n }\n\n async displayDialogue() {\n const modal = await Modal.create({\n large: true,\n title: getString('mediamanagerproperties', 'tiny_mediacms'),\n body: Templates.render('tiny_mediacms/mm2_iframe', {\n src: this.getIframeURL()\n }),\n removeOnClose: true,\n show: true,\n });\n modal.getRoot().on(ModalEvents.bodyRendered, () => {\n this.selectFirstElement();\n });\n\n document.querySelector('.modal-lg').style.cssText = `max-width: 850px`;\n return modal;\n }\n\n // It will select the first element in the file manager.\n selectFirstElement() {\n const iframe = document.getElementById('mm2-iframe');\n iframe.addEventListener('load', function() {\n let intervalId = setInterval(function() {\n const iDocument = iframe.contentWindow.document;\n if (iDocument.querySelector('.filemanager')) {\n const firstFocusableElement = iDocument.querySelector('.fp-navbar a:not([disabled])');\n if (firstFocusableElement) {\n firstFocusableElement.focus();\n }\n clearInterval(intervalId);\n }\n }, 200);\n });\n }\n\n getIframeURL() {\n const url = new URL(`${Config.wwwroot}/lib/editor/tiny/plugins/mediacms/manage.php`);\n url.searchParams.append('elementid', this.editor.getElement().id);\n for (const key in this.area) {\n url.searchParams.append(key, this.area[key]);\n }\n return url.toString();\n }\n}\n"],"names":["constructor","editor","data","area","params","itemid","fpoptions","image","modal","Modal","create","large","title","body","Templates","render","src","this","getIframeURL","removeOnClose","show","getRoot","on","ModalEvents","bodyRendered","selectFirstElement","document","querySelector","style","cssText","iframe","getElementById","addEventListener","intervalId","setInterval","iDocument","contentWindow","firstFocusableElement","focus","clearInterval","url","URL","Config","wwwroot","searchParams","append","getElement","id","key","toString"],"mappings":"mmDAmCIA,YAAYC,sCAHH,kCACF,WAGEA,OAASA,aACRC,MAAO,oBAAQD,aAChBE,KAAOD,KAAKE,OAAOD,UACnBA,KAAKE,OAASH,KAAKI,UAAUC,MAAMF,qCAIlCG,YAAcC,eAAMC,OAAO,CAC7BC,OAAO,EACPC,OAAO,kBAAU,yBAA0B,iBAC3CC,KAAMC,mBAAUC,OAAO,2BAA4B,CAC/CC,IAAKC,KAAKC,iBAEdC,eAAe,EACfC,MAAM,WAEVZ,MAAMa,UAAUC,GAAGC,YAAYC,cAAc,UACpCC,wBAGTC,SAASC,cAAc,aAAaC,MAAMC,2BACnCrB,MAIXiB,2BACUK,OAASJ,SAASK,eAAe,cACvCD,OAAOE,iBAAiB,QAAQ,eACxBC,WAAaC,aAAY,iBACnBC,UAAYL,OAAOM,cAAcV,YACnCS,UAAUR,cAAc,gBAAiB,OACnCU,sBAAwBF,UAAUR,cAAc,gCAClDU,uBACAA,sBAAsBC,QAE1BC,cAAcN,eAEnB,QAIXf,qBACUsB,IAAM,IAAIC,cAAOC,gBAAOC,yDAC9BH,IAAII,aAAaC,OAAO,YAAa5B,KAAKhB,OAAO6C,aAAaC,QACzD,MAAMC,OAAO/B,KAAKd,KACnBqC,IAAII,aAAaC,OAAOG,IAAK/B,KAAKd,KAAK6C,aAEpCR,IAAIS"}

View File

@@ -0,0 +1,11 @@
define("tiny_mediacms/options",["exports","editor_tiny/options","./common"],(function(_exports,_options,_common){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.register=_exports.getPermissions=_exports.getLti=_exports.getImagePermissions=_exports.getEmbedPermissions=_exports.getData=void 0;
/**
* Options helper for Tiny Media plugin.
*
* @module tiny_mediacms/options
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
const dataName=(0,_options.getPluginOptionName)(_common.pluginName,"data"),permissionsName=(0,_options.getPluginOptionName)(_common.pluginName,"permissions"),ltiName=(0,_options.getPluginOptionName)(_common.pluginName,"lti");_exports.register=editor=>{const registerOption=editor.options.register;registerOption(permissionsName,{processor:"object",default:{image:{filepicker:!1}}}),registerOption(dataName,{processor:"object",default:{mediacmsApiUrl:"",mediacmsBaseUrl:"",mediacmsPageSize:12,autoConvertEnabled:!0,autoConvertBaseUrl:"",autoConvertOptions:{showTitle:!0,linkTitle:!0,showRelated:!0,showUserAvatar:!0}}}),registerOption(ltiName,{processor:"object",default:{toolId:0,courseId:0,contentItemUrl:""}})};const getPermissions=editor=>editor.options.get(permissionsName);_exports.getPermissions=getPermissions;_exports.getImagePermissions=editor=>getPermissions(editor).image;_exports.getEmbedPermissions=editor=>getPermissions(editor).embed;_exports.getData=editor=>editor.options.get(dataName);_exports.getLti=editor=>editor.options.get(ltiName)}));
//# sourceMappingURL=options.min.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"options.min.js","sources":["../src/options.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Options helper for Tiny Media plugin.\n *\n * @module tiny_mediacms/options\n * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {getPluginOptionName} from 'editor_tiny/options';\nimport {pluginName} from './common';\n\nconst dataName = getPluginOptionName(pluginName, 'data');\nconst permissionsName = getPluginOptionName(pluginName, 'permissions');\nconst ltiName = getPluginOptionName(pluginName, 'lti');\n\n/**\n * Register the options for the Tiny Media plugin.\n *\n * @param {TinyMCE} editor\n */\nexport const register = (editor) => {\n const registerOption = editor.options.register;\n\n registerOption(permissionsName, {\n processor: 'object',\n \"default\": {\n image: {\n filepicker: false,\n }\n },\n });\n\n registerOption(dataName, {\n processor: 'object',\n \"default\": {\n // MediaCMS video library configuration\n mediacmsApiUrl: '', // e.g., 'https://deic.mediacms.io/api/v1/media'\n mediacmsBaseUrl: '', // e.g., 'https://deic.mediacms.io'\n mediacmsPageSize: 12,\n // Auto-conversion settings\n autoConvertEnabled: true, // Enable/disable auto-conversion of pasted MediaCMS URLs\n autoConvertBaseUrl: '', // Base URL to restrict auto-conversion (empty = allow all MediaCMS domains)\n autoConvertOptions: {\n // Default embed options for auto-converted videos\n showTitle: true,\n linkTitle: true,\n showRelated: true,\n showUserAvatar: true,\n },\n },\n });\n\n registerOption(ltiName, {\n processor: 'object',\n \"default\": {\n // LTI configuration for MediaCMS iframe library\n toolId: 0, // LTI external tool ID\n courseId: 0, // Current course ID\n contentItemUrl: '', // URL to /mod/lti/contentitem.php for Deep Linking\n },\n });\n};\n\n/**\n * Get the permissions configuration for the Tiny Media plugin.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getPermissions = (editor) => editor.options.get(permissionsName);\n\n/**\n * Get the permissions configuration for the Tiny Media plugin.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getImagePermissions = (editor) => getPermissions(editor).image;\n\n/**\n * Get the permissions configuration for the Tiny Media plugin.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getEmbedPermissions = (editor) => getPermissions(editor).embed;\n\n/**\n * Get the data configuration for the Media Manager.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getData = (editor) => editor.options.get(dataName);\n\n/**\n * Get the LTI configuration for the MediaCMS iframe library.\n *\n * @param {TinyMCE} editor\n * @returns {object}\n */\nexport const getLti = (editor) => editor.options.get(ltiName);\n"],"names":["dataName","pluginName","permissionsName","ltiName","editor","registerOption","options","register","processor","image","filepicker","mediacmsApiUrl","mediacmsBaseUrl","mediacmsPageSize","autoConvertEnabled","autoConvertBaseUrl","autoConvertOptions","showTitle","linkTitle","showRelated","showUserAvatar","toolId","courseId","contentItemUrl","getPermissions","get","embed"],"mappings":";;;;;;;;MA0BMA,UAAW,gCAAoBC,mBAAY,QAC3CC,iBAAkB,gCAAoBD,mBAAY,eAClDE,SAAU,gCAAoBF,mBAAY,yBAOvBG,eACfC,eAAiBD,OAAOE,QAAQC,SAEtCF,eAAeH,gBAAiB,CAC5BM,UAAW,iBACA,CACPC,MAAO,CACHC,YAAY,MAKxBL,eAAeL,SAAU,CACrBQ,UAAW,iBACA,CAEPG,eAAgB,GAChBC,gBAAiB,GACjBC,iBAAkB,GAElBC,oBAAoB,EACpBC,mBAAoB,GACpBC,mBAAoB,CAEhBC,WAAW,EACXC,WAAW,EACXC,aAAa,EACbC,gBAAgB,MAK5Bf,eAAeF,QAAS,CACpBK,UAAW,iBACA,CAEPa,OAAQ,EACRC,SAAU,EACVC,eAAgB,aAWfC,eAAkBpB,QAAWA,OAAOE,QAAQmB,IAAIvB,qFAQzBE,QAAWoB,eAAepB,QAAQK,mCAQlCL,QAAWoB,eAAepB,QAAQsB,uBAQ9CtB,QAAWA,OAAOE,QAAQmB,IAAIzB,0BAQ/BI,QAAWA,OAAOE,QAAQmB,IAAItB"}

View File

@@ -0,0 +1,10 @@
define("tiny_mediacms/plugin",["exports","editor_tiny/loader","editor_tiny/utils","./common","./commands","./configuration","./options","./autoconvert"],(function(_exports,_loader,_utils,_common,Commands,Configuration,Options,_autoconvert){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}
/**
* Tiny Media plugin for Moodle.
*
* @module tiny_mediacms/plugin
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,Commands=_interopRequireWildcard(Commands),Configuration=_interopRequireWildcard(Configuration),Options=_interopRequireWildcard(Options);const isMediaCMSUrl=url=>{if(!url)return!1;try{const urlObj=new URL(url);return("/embed"===urlObj.pathname||"/view"===urlObj.pathname)&&urlObj.searchParams.has("m")}catch(e){return!1}},MEDIACMS_URL_PATTERN=/(^|>|\s)(https?:\/\/[^\s<>"]+\/(?:embed|view)\?m=[^\s<>"]+)(<|\s|$)/g;var _default=new Promise((async resolve=>{const[tinyMCE,setupCommands,pluginMetadata]=await Promise.all([(0,_loader.getTinyMCE)(),Commands.getSetup(),(0,_utils.getPluginMetadata)(_common.component,_common.pluginName)]);tinyMCE.PluginManager.add("".concat(_common.component,"/plugin"),(editor=>(Options.register(editor),setupCommands(editor),(0,_autoconvert.setupAutoConvert)(editor),editor.on("BeforeSetContent",(e=>{e.content&&"string"==typeof e.content&&(e.content=e.content.replace(MEDIACMS_URL_PATTERN,((match,before,url,after)=>isMediaCMSUrl(url)?before+(url=>{let embedUrl=url;try{const urlObj=new URL(url);"/view"===urlObj.pathname&&(urlObj.pathname="/embed",embedUrl=urlObj.toString())}catch(e){}return'<iframe src="'.concat(embedUrl,'" ')+'style="width: 100%; aspect-ratio: 16 / 9; display: block; border: 0;" allowfullscreen="allowfullscreen"></iframe>'})(url)+after:match)))})),editor.on("GetContent",(e=>{if("html"===e.format){const tempDiv=document.createElement("div");tempDiv.innerHTML=e.content,tempDiv.querySelectorAll(".tiny-mediacms-edit-btn").forEach((btn=>btn.remove())),tempDiv.querySelectorAll("iframe").forEach((iframe=>{const src=iframe.getAttribute("src");if(isMediaCMSUrl(src)){const wrapper=iframe.closest(".tiny-mediacms-iframe-wrapper")||iframe.closest(".tiny-iframe-responsive"),urlText=document.createTextNode(src),p=document.createElement("p");p.appendChild(urlText),wrapper?(wrapper.parentNode.insertBefore(p,wrapper),wrapper.remove()):(iframe.parentNode.insertBefore(p,iframe),iframe.remove())}})),tempDiv.querySelectorAll(".tiny-mediacms-iframe-wrapper").forEach((wrapper=>{const iframe=wrapper.querySelector("iframe");iframe&&wrapper.parentNode.insertBefore(iframe,wrapper),wrapper.remove()})),tempDiv.querySelectorAll(".tiny-iframe-responsive").forEach((wrapper=>{const iframe=wrapper.querySelector("iframe");iframe&&wrapper.parentNode.insertBefore(iframe,wrapper),wrapper.remove()})),e.content=tempDiv.innerHTML}})),pluginMetadata))),resolve(["".concat(_common.component,"/plugin"),Configuration])}));return _exports.default=_default,_exports.default}));
//# sourceMappingURL=plugin.min.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
define("tiny_mediacms/selectors",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={IMAGE:{actions:{submit:".tiny_imagecms_urlentrysubmit",imageBrowser:".openimagecmsbrowser",addUrl:".tiny_imagecms_addurl",deleteImage:".tiny_imagecms_deleteicon"},elements:{form:"form.tiny_imagecms_form",alignSettings:".tiny_imagecms_button",alt:".tiny_imagecms_altentry",altWarning:".tiny_imagecms_altwarning",height:".tiny_imagecms_heightentry",width:".tiny_imagecms_widthentry",url:".tiny_imagecms_urlentry",urlWarning:".tiny_imagecms_urlwarning",size:".tiny_imagecms_size",presentation:".tiny_imagecms_presentation",constrain:".tiny_imagecms_constrain",customStyle:".tiny_imagecms_customstyle",preview:".tiny_imagecms_preview",previewBox:".tiny_imagecms_preview_box",loaderIcon:".tiny_imagecms_loader",loaderIconContainer:".tiny_imagecms_loader_container",insertImage:".tiny_imagecms_insert_image",modalFooter:".modal-footer",dropzoneContainer:".tiny_imagecms_dropzone_container",fileInput:"#tiny_imagecms_fileinput",fileNameLabel:".tiny_imagecms_filename",sizeOriginal:".tiny_imagecms_sizeoriginal",sizeCustom:".tiny_imagecms_sizecustom",properties:".tiny_imagecms_properties"},styles:{responsive:"img-fluid"}},EMBED:{actions:{submit:".tiny_mediacms_submit",mediaBrowser:".openmediacmsbrowser"},elements:{form:"form.tiny_mediacms_form",source:".tiny_mediacms_source",track:".tiny_mediacms_track",mediaSource:".tiny_mediacms_media_source",linkSource:".tiny_mediacms_link_source",linkSize:".tiny_mediacms_link_size",posterSource:".tiny_mediacms_poster_source",posterSize:".tiny_mediacms_poster_size",displayOptions:".tiny_mediacms_display_options",name:".tiny_mediacms_name_entry",title:".tiny_mediacms_title_entry",url:".tiny_mediacms_url_entry",width:".tiny_mediacms_width_entry",height:".tiny_mediacms_height_entry",trackSource:".tiny_mediacms_track_source",trackKind:".tiny_mediacms_track_kind_entry",trackLabel:".tiny_mediacms_track_label_entry",trackLang:".tiny_mediacms_track_lang_entry",trackDefault:".tiny_mediacms_track_default",mediaControl:".tiny_mediacms_controls",mediaAutoplay:".tiny_mediacms_autoplay",mediaMute:".tiny_mediacms_mute",mediaLoop:".tiny_mediacms_loop",advancedSettings:".tiny_mediacms_advancedsettings",linkTab:'li[data-medium-type="link"]',videoTab:'li[data-medium-type="video"]',audioTab:'li[data-medium-type="audio"]',linkPane:'.tab-pane[data-medium-type="link"]',videoPane:'.tab-pane[data-medium-type="video"]',audioPane:'.tab-pane[data-medium-type="audio"]',trackSubtitlesTab:'li[data-track-kind="subtitles"]',trackCaptionsTab:'li[data-track-kind="captions"]',trackDescriptionsTab:'li[data-track-kind="descriptions"]',trackChaptersTab:'li[data-track-kind="chapters"]',trackMetadataTab:'li[data-track-kind="metadata"]',trackSubtitlesPane:'.tab-pane[data-track-kind="subtitles"]',trackCaptionsPane:'.tab-pane[data-track-kind="captions"]',trackDescriptionsPane:'.tab-pane[data-track-kind="descriptions"]',trackChaptersPane:'.tab-pane[data-track-kind="chapters"]',trackMetadataPane:'.tab-pane[data-track-kind="metadata"]'},mediaTypes:{link:"LINK",video:"VIDEO",audio:"AUDIO"},trackKinds:{subtitles:"SUBTITLES",captions:"CAPTIONS",descriptions:"DESCRIPTIONS",chapters:"CHAPTERS",metadata:"METADATA"}},IFRAME:{actions:{remove:'[data-action="remove"]'},elements:{form:"form.tiny_iframecms_form",url:".tiny_iframecms_url",urlWarning:".tiny_iframecms_url_warning",showTitle:".tiny_iframecms_showtitle",linkTitle:".tiny_iframecms_linktitle",showRelated:".tiny_iframecms_showrelated",showUserAvatar:".tiny_iframecms_showuseravatar",responsive:".tiny_iframecms_responsive",startAt:".tiny_iframecms_startat",startAtEnabled:".tiny_iframecms_startat_enabled",aspectRatio:".tiny_iframecms_aspectratio",width:".tiny_iframecms_width",height:".tiny_iframecms_height",preview:".tiny_iframecms_preview",previewContainer:".tiny_iframecms_preview_container",tabs:".tiny_iframecms_tabs",tabUrlBtn:".tiny_iframecms_tab_url_btn",tabIframeLibraryBtn:".tiny_iframecms_tab_iframe_library_btn",paneUrl:".tiny_iframecms_pane_url",paneIframeLibrary:".tiny_iframecms_pane_iframe_library",iframeLibraryContainer:".tiny_iframecms_iframe_library_container",iframeLibraryPlaceholder:".tiny_iframecms_iframe_library_placeholder",iframeLibraryLoading:".tiny_iframecms_iframe_library_loading",iframeLibraryFrame:".tiny_iframecms_iframe_library_frame"},aspectRatios:{"16:9":{width:560,height:315},"4:3":{width:560,height:420},"1:1":{width:400,height:400},custom:null}}},_exports.default}));
//# sourceMappingURL=selectors.min.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,10 @@
define("tiny_mediacms/usedfiles",["exports","core/templates","core/config"],(function(_exports,Templates,_config){var obj;function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,Templates=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}
/**
* Tiny Media Manager usedfiles.
*
* @module tiny_mediacms/usedfiles
* @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/(Templates),_config=(obj=_config)&&obj.__esModule?obj:{default:obj};class UsedFileManager{constructor(files,userContext,itemId,elementId){this.files=files,this.userContext=userContext,this.itemId=itemId,this.elementId=elementId}getElementId(){return this.elementId}getUsedFiles(){const editor=window.parent.tinymce.EditorManager.get(this.getElementId());if(!editor)return window.console.error("Editor not found for ".concat(this.getElementId())),[];const content=editor.getContent(),baseUrl="".concat(_config.default.wwwroot,"/draftfile.php/").concat(this.userContext,"/user/draft/").concat(this.itemId,"/"),pattern=new RegExp("[\"']"+baseUrl.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&")+"(?<filename>.+?)[\\?\"']","gm");return[...content.matchAll(pattern)].map((match=>decodeURIComponent(match.groups.filename)))}findUnusedFiles(usedFiles){return Object.entries(this.files).filter((_ref=>{let[filename]=_ref;return!usedFiles.includes(filename)})).map((_ref2=>{let[filename]=_ref2;return filename}))}findMissingFiles(usedFiles){return usedFiles.filter((filename=>!this.files.hasOwnProperty(filename)))}updateFiles(){const form=document.querySelector("form"),usedFiles=this.getUsedFiles(),unusedFiles=this.findUnusedFiles(usedFiles),missingFiles=this.findMissingFiles(usedFiles);return form.querySelectorAll('input[type=checkbox][name^="deletefile"]').forEach((checkbox=>{unusedFiles.includes(checkbox.dataset.filename)||checkbox.closest(".fitem").remove()})),form.classList.toggle("has-missing-files",!!missingFiles.length),form.classList.toggle("has-unused-files",!!unusedFiles.length),Templates.renderForPromise("tiny_mediacms/missingfiles",{missingFiles:missingFiles}).then((_ref3=>{let{html:html,js:js}=_ref3;Templates.replaceNodeContents(form.querySelector(".missing-files"),html,js)}))}}_exports.init=(files,usercontext,itemid,elementid)=>{const manager=new UsedFileManager(files,usercontext,itemid,elementid);return manager.updateFiles(),manager}}));
//# sourceMappingURL=usedfiles.min.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,264 @@
// 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 MediaCMS Auto-convert module.
*
* This module automatically converts pasted MediaCMS URLs into embedded videos.
* When a user pastes a MediaCMS video URL (e.g., https://deic.mediacms.io/view?m=JpBd1Zvdl),
* it will be automatically converted to an iframe embed.
*
* @module tiny_mediacms/autoconvert
* @copyright 2024
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {getData} from './options';
/**
* Regular expression patterns for MediaCMS URLs.
* Matches URLs like:
* - https://deic.mediacms.io/view?m=JpBd1Zvdl
* - https://example.mediacms.io/view?m=VIDEO_ID
* - Custom domains configured in the plugin
*/
const MEDIACMS_VIEW_URL_PATTERN = /^(https?:\/\/[^\/]+)\/view\?m=([a-zA-Z0-9_-]+)$/;
/**
* Check if a string is a valid MediaCMS view URL.
*
* @param {string} text - The text to check
* @returns {Object|null} - Parsed URL info or null if not a valid MediaCMS URL
*/
const parseMediaCMSUrl = (text) => {
if (!text || typeof text !== 'string') {
return null;
}
const trimmed = text.trim();
// Check for MediaCMS view URL pattern
const match = trimmed.match(MEDIACMS_VIEW_URL_PATTERN);
if (match) {
return {
baseUrl: match[1],
videoId: match[2],
originalUrl: trimmed,
};
}
return null;
};
/**
* Check if the pasted URL's domain is allowed based on configuration.
*
* @param {Object} parsed - Parsed URL info
* @param {Object} config - Plugin configuration
* @returns {boolean} - True if the domain is allowed
*/
const isDomainAllowed = (parsed, config) => {
// If no specific base URL is configured, allow all MediaCMS domains
const configuredBaseUrl = config.autoConvertBaseUrl || config.mediacmsBaseUrl;
if (!configuredBaseUrl) {
return true;
}
// Check if the URL's base matches the configured base URL
try {
const configuredUrl = new URL(configuredBaseUrl);
const pastedUrl = new URL(parsed.baseUrl);
return configuredUrl.host === pastedUrl.host;
} catch (e) {
// If URL parsing fails, allow the conversion
return true;
}
};
/**
* Generate the iframe embed HTML for a MediaCMS video.
*
* @param {Object} parsed - Parsed URL info
* @param {Object} options - Embed options
* @returns {string} - The iframe HTML
*/
const generateEmbedHtml = (parsed, options = {}) => {
// Build the embed URL with default options
const embedUrl = new URL(`${parsed.baseUrl}/embed`);
embedUrl.searchParams.set('m', parsed.videoId);
// Apply default options (all enabled by default for best user experience)
embedUrl.searchParams.set('showTitle', options.showTitle !== false ? '1' : '0');
embedUrl.searchParams.set('showRelated', options.showRelated !== false ? '1' : '0');
embedUrl.searchParams.set('showUserAvatar', options.showUserAvatar !== false ? '1' : '0');
embedUrl.searchParams.set('linkTitle', options.linkTitle !== false ? '1' : '0');
// Generate responsive iframe HTML matching the template output format.
// Uses aspect-ratio CSS for responsive sizing (16:9 default).
// The wrapper will be added by editor for UI (edit button), then stripped on save.
const html = `<iframe src="${embedUrl.toString()}" ` +
`style="width: 100%; aspect-ratio: 16 / 9; display: block; border: 0;" ` +
`allowfullscreen="allowfullscreen"></iframe>`;
return html;
};
/**
* Set up auto-conversion for the editor.
* This registers event handlers to detect pasted MediaCMS URLs.
*
* @param {TinyMCE} editor - The TinyMCE editor instance
*/
export const setupAutoConvert = (editor) => {
const config = getData(editor) || {};
// Check if auto-convert is enabled (default: true)
if (config.autoConvertEnabled === false) {
return;
}
// Handle paste events
editor.on('paste', (e) => {
handlePasteEvent(editor, e, config);
});
// Also handle input events for drag-and-drop text or keyboard paste
editor.on('input', (e) => {
handleInputEvent(editor, e, config);
});
};
/**
* Handle paste events to detect and convert MediaCMS URLs.
*
* @param {TinyMCE} editor - The TinyMCE editor instance
* @param {Event} e - The paste event
* @param {Object} config - Plugin configuration
*/
const handlePasteEvent = (editor, e, config) => {
// Get pasted text from clipboard
const clipboardData = e.clipboardData || window.clipboardData;
if (!clipboardData) {
return;
}
// Try to get plain text first
const text = clipboardData.getData('text/plain') || clipboardData.getData('text');
if (!text) {
return;
}
// Check if it's a MediaCMS URL
const parsed = parseMediaCMSUrl(text);
if (!parsed) {
return;
}
// Check if domain is allowed
if (!isDomainAllowed(parsed, config)) {
return;
}
// Prevent default paste behavior
e.preventDefault();
e.stopPropagation();
// Generate and insert the embed HTML
const embedHtml = generateEmbedHtml(parsed, config.autoConvertOptions || {});
// Use a slight delay to ensure the editor is ready
setTimeout(() => {
editor.insertContent(embedHtml);
// Move cursor after the inserted content
editor.selection.collapse(false);
}, 0);
};
/**
* Handle input events to catch URLs that might have been pasted without triggering paste event.
* This is a fallback for certain browsers/scenarios.
*
* @param {TinyMCE} editor - The TinyMCE editor instance
* @param {Event} e - The input event
* @param {Object} config - Plugin configuration
*/
const handleInputEvent = (editor, e, config) => {
// Only process inputType 'insertFromPaste' if paste event didn't catch it
if (e.inputType !== 'insertFromPaste' && e.inputType !== 'insertText') {
return;
}
// Get the current node and check if it contains just a URL
const node = editor.selection.getNode();
if (!node || node.nodeName !== 'P') {
return;
}
// Check if the paragraph contains only a MediaCMS URL
const text = node.textContent || '';
const parsed = parseMediaCMSUrl(text);
if (!parsed || !isDomainAllowed(parsed, config)) {
return;
}
// Don't convert if there's other content in the paragraph
const trimmedHtml = node.innerHTML.trim();
if (trimmedHtml !== text.trim() && !trimmedHtml.startsWith(text.trim())) {
return;
}
// Generate the embed HTML
const embedHtml = generateEmbedHtml(parsed, config.autoConvertOptions || {});
// Replace the paragraph content with the embed
// Use a slight delay to let the input event complete
setTimeout(() => {
// Re-check that the node still contains the URL (user might have typed more)
const currentText = node.textContent || '';
const currentParsed = parseMediaCMSUrl(currentText);
if (currentParsed && currentParsed.originalUrl === parsed.originalUrl) {
// Select and replace the entire node
editor.selection.select(node);
editor.insertContent(embedHtml);
}
}, 100);
};
/**
* Check if a text is a MediaCMS URL (public helper).
*
* @param {string} text - The text to check
* @returns {boolean} - True if it's a MediaCMS URL
*/
export const isMediaCMSUrl = (text) => {
return parseMediaCMSUrl(text) !== null;
};
/**
* Convert a MediaCMS URL to embed HTML (public helper).
*
* @param {string} url - The MediaCMS URL
* @param {Object} options - Embed options
* @returns {string|null} - The embed HTML or null if not a valid URL
*/
export const convertToEmbed = (url, options = {}) => {
const parsed = parseMediaCMSUrl(url);
if (!parsed) {
return null;
}
return generateEmbedHtml(parsed, options);
};

View File

@@ -0,0 +1,282 @@
// 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 commands.
*
* @module tiny_mediacms/commands
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {getStrings} from 'core/str';
import {
component,
iframeButtonName,
iframeMenuItemName,
iframeIcon,
} from './common';
import IframeEmbed from './iframeembed';
import {getButtonImage} from 'editor_tiny/utils';
const isIframe = (node) => node.nodeName.toLowerCase() === 'iframe' ||
(node.classList && node.classList.contains('tiny-iframe-responsive')) ||
(node.classList && node.classList.contains('tiny-mediacms-iframe-wrapper'));
/**
* Wrap iframes with overlay containers that allow hover detection.
* Since iframes capture mouse events, we add an invisible overlay on top
* that shows the edit button on hover.
*
* @param {TinyMCE} editor - The editor instance
* @param {Function} handleIframeAction - The action to perform when clicking the button
*/
const setupIframeOverlays = (editor, handleIframeAction) => {
/**
* Process all iframes in the editor and add overlay wrappers.
*/
const processIframes = () => {
const editorBody = editor.getBody();
if (!editorBody) {
return;
}
const iframes = editorBody.querySelectorAll('iframe');
iframes.forEach((iframe) => {
// Skip if already wrapped
if (iframe.parentElement?.classList.contains('tiny-mediacms-iframe-wrapper')) {
return;
}
// Skip TinyMCE internal iframes
if (iframe.hasAttribute('data-mce-object') || iframe.hasAttribute('data-mce-placeholder')) {
return;
}
// Create wrapper div
const wrapper = editor.getDoc().createElement('div');
wrapper.className = 'tiny-mediacms-iframe-wrapper';
wrapper.setAttribute('contenteditable', 'false');
// Create edit button (positioned inside wrapper, over the iframe)
const editBtn = editor.getDoc().createElement('button');
editBtn.className = 'tiny-mediacms-edit-btn';
editBtn.setAttribute('type', 'button');
editBtn.setAttribute('title', 'Edit video embed options');
// Use clean inline SVG to avoid TinyMCE wrapper issues
editBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">' +
'<circle cx="50" cy="50" r="48" fill="#2EAF5A"/>' +
'<polygon points="38,28 38,72 75,50" fill="#FFFFFF"/>' +
'</svg>';
// Wrap the iframe: insert wrapper, move iframe into it, add button
iframe.parentNode.insertBefore(wrapper, iframe);
wrapper.appendChild(iframe);
wrapper.appendChild(editBtn);
});
};
/**
* Add CSS styles for hover effects to the editor's document.
*/
const addStyles = () => {
const editorDoc = editor.getDoc();
if (!editorDoc) {
return;
}
// Check if styles already added
if (editorDoc.getElementById('tiny-mediacms-overlay-styles')) {
return;
}
const style = editorDoc.createElement('style');
style.id = 'tiny-mediacms-overlay-styles';
style.textContent = `
.tiny-mediacms-iframe-wrapper {
display: inline-block;
position: relative;
line-height: 0;
vertical-align: top;
}
.tiny-mediacms-iframe-wrapper iframe {
display: block;
}
.tiny-mediacms-edit-btn {
position: absolute;
top: 48px;
left: 6px;
width: 28px;
height: 28px;
background: #ffffff;
border: none;
border-radius: 50%;
cursor: pointer;
z-index: 10;
padding: 0;
margin: 0;
box-shadow: 0 2px 6px rgba(0,0,0,0.35);
transition: transform 0.15s, box-shadow 0.15s;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.tiny-mediacms-edit-btn:hover {
transform: scale(1.15);
box-shadow: 0 3px 10px rgba(0,0,0,0.45);
}
.tiny-mediacms-edit-btn svg {
width: 18px !important;
height: 18px !important;
display: block !important;
}
`;
editorDoc.head.appendChild(style);
};
/**
* Handle click on the edit button.
*
* @param {Event} e - The click event
*/
const handleOverlayClick = (e) => {
const target = e.target;
// Check if clicked on edit button or its child (svg/path)
const editBtn = target.closest('.tiny-mediacms-edit-btn');
if (!editBtn) {
return;
}
e.preventDefault();
e.stopPropagation();
// Find the associated wrapper and iframe
const wrapper = editBtn.closest('.tiny-mediacms-iframe-wrapper');
if (!wrapper) {
return;
}
const iframe = wrapper.querySelector('iframe');
if (!iframe) {
return;
}
// Select the wrapper so TinyMCE knows which element is selected
editor.selection.select(wrapper);
// Open the edit dialog
handleIframeAction();
};
// Setup on editor init
editor.on('init', () => {
addStyles();
processIframes();
// Handle clicks on the overlay
editor.getBody().addEventListener('click', handleOverlayClick);
});
// Re-process when content changes
editor.on('SetContent', () => {
processIframes();
});
// Re-process when content is pasted
editor.on('PastePostProcess', () => {
setTimeout(processIframes, 100);
});
// Re-process after undo/redo
editor.on('Undo Redo', () => {
processIframes();
});
// Re-process on any content change (covers modal updates)
editor.on('Change', () => {
setTimeout(processIframes, 50);
});
// Re-process when node changes (selection changes)
editor.on('NodeChange', () => {
processIframes();
});
};
const registerIframeCommand = (editor, iframeButtonText, iframeButtonImage) => {
const handleIframeAction = () => {
const iframeEmbed = new IframeEmbed(editor);
iframeEmbed.displayDialogue();
};
// Register the iframe icon
editor.ui.registry.addIcon(iframeIcon, iframeButtonImage.html);
// Register the Menu Button as a toggle.
// This means that when highlighted over an existing iframe element it will show as toggled on.
editor.ui.registry.addToggleButton(iframeButtonName, {
icon: iframeIcon,
tooltip: iframeButtonText,
onAction: handleIframeAction,
onSetup: api => {
return editor.selection.selectorChangedWithUnbind(
'iframe:not([data-mce-object]):not([data-mce-placeholder]),.tiny-iframe-responsive,.tiny-mediacms-iframe-wrapper',
api.setActive
).unbind;
}
});
editor.ui.registry.addMenuItem(iframeMenuItemName, {
icon: iframeIcon,
text: iframeButtonText,
onAction: handleIframeAction,
});
editor.ui.registry.addContextToolbar(iframeButtonName, {
predicate: isIframe,
items: iframeButtonName,
position: 'node',
scope: 'node'
});
editor.ui.registry.addContextMenu(iframeButtonName, {
update: isIframe,
});
// Setup iframe overlays with edit button on hover
setupIframeOverlays(editor, handleIframeAction);
};
export const getSetup = async() => {
const [
iframeButtonText,
] = await getStrings([
'iframebuttontitle',
].map((key) => ({key, component})));
const [
iframeButtonImage,
] = await Promise.all([
getButtonImage('icon', component),
]);
// Note: The function returned here must be synchronous and cannot use promises.
// All promises must be resolved prior to returning the function.
return (editor) => {
registerIframeCommand(editor, iframeButtonText, iframeButtonImage);
};
};

View File

@@ -0,0 +1,30 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Tiny Media common values.
*
* @module tiny_mediacms/common
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
export default {
pluginName: 'tiny_mediacms/plugin',
component: 'tiny_mediacms',
iframeButtonName: 'tiny_mediacms_iframe',
iframeMenuItemName: 'tiny_mediacms_iframe',
iframeIcon: 'tiny_mediacms_iframe',
};

View File

@@ -0,0 +1,60 @@
// 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 configuration.
*
* @module tiny_mediacms/configuration
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {
iframeButtonName,
iframeMenuItemName,
} from './common';
import {
addContextmenuItem,
} from 'editor_tiny/utils';
const configureMenu = (menu) => {
// Add the Iframe Embed to the insert menu.
menu.insert.items = `${iframeMenuItemName} ${menu.insert.items}`;
return menu;
};
const configureToolbar = (toolbar) => {
// The toolbar contains an array of named sections.
// The Moodle integration ensures that there is a section called 'content'.
return toolbar.map((section) => {
if (section.name === 'content') {
// Insert the iframe button at the start of it.
section.items.unshift(iframeButtonName);
}
return section;
});
};
export const configure = (instanceConfig) => {
// Update the instance configuration to add the Iframe Embed menu option to the menus and toolbars.
return {
contextmenu: addContextmenuItem(instanceConfig.contextmenu, iframeButtonName),
menu: configureMenu(instanceConfig.menu),
toolbar: configureToolbar(instanceConfig.toolbar),
};
};

View File

@@ -0,0 +1,467 @@
// 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 Embed class for Moodle.
*
* @module tiny_mediacms/embed
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Templates from 'core/templates';
import {
getString,
getStrings,
} from 'core/str';
import * as ModalEvents from 'core/modal_events';
import {displayFilepicker} from 'editor_tiny/utils';
import {getCurrentLanguage, getMoodleLang} from 'editor_tiny/options';
import {component} from "./common";
import EmbedModal from './embedmodal';
import Selectors from './selectors';
import {getEmbedPermissions} from './options';
import {getFilePicker} from 'editor_tiny/options';
export default class MediaEmbed {
editor = null;
canShowFilePicker = false;
canShowFilePickerPoster = false;
canShowFilePickerTrack = false;
/**
* @property {Object} The names of the alignment options.
*/
helpStrings = null;
/**
* @property {boolean} Indicate that the user is updating the media or not.
*/
isUpdating = false;
/**
* @property {Object} The currently selected media.
*/
selectedMedia = null;
constructor(editor) {
const permissions = getEmbedPermissions(editor);
// Indicates whether the file picker can be shown.
this.canShowFilePicker = permissions.filepicker && (typeof getFilePicker(editor, 'media') !== 'undefined');
this.canShowFilePickerPoster = permissions.filepicker && (typeof getFilePicker(editor, 'image') !== 'undefined');
this.canShowFilePickerTrack = permissions.filepicker && (typeof getFilePicker(editor, 'subtitle') !== 'undefined');
this.editor = editor;
}
async getHelpStrings() {
if (!this.helpStrings) {
const [addSource, tracks, subtitles, captions, descriptions, chapters, metadata] = await getStrings([
'addsource_help',
'tracks_help',
'subtitles_help',
'captions_help',
'descriptions_help',
'chapters_help',
'metadata_help',
].map((key) => ({
key,
component,
})));
this.helpStrings = {addSource, tracks, subtitles, captions, descriptions, chapters, metadata};
}
return this.helpStrings;
}
async getTemplateContext(data) {
const languages = this.prepareMoodleLang();
const helpIcons = Array.from(Object.entries(await this.getHelpStrings())).forEach(([key, text]) => {
data[`${key.toLowerCase()}helpicon`] = {text};
});
return Object.assign({}, {
elementid: this.editor.getElement().id,
showfilepicker: this.canShowFilePicker,
showfilepickerposter: this.canShowFilePickerPoster,
showfilepickertrack: this.canShowFilePickerTrack,
langsinstalled: languages.installed,
langsavailable: languages.available,
link: true,
video: false,
audio: false,
isupdating: this.isUpdating,
}, data, helpIcons);
}
async displayDialogue() {
this.selectedMedia = this.getSelectedMedia();
const data = Object.assign({}, this.getCurrentEmbedData());
this.isUpdating = Object.keys(data).length !== 0;
this.currentModal = await EmbedModal.create({
title: getString('createmedia', 'tiny_mediacms'),
templateContext: await this.getTemplateContext(data),
});
await this.registerEventListeners(this.currentModal);
}
getCurrentEmbedData() {
const properties = this.getMediumProperties();
if (!properties) {
return {};
}
const processedProperties = {};
processedProperties[properties.type.toLowerCase()] = properties;
processedProperties.link = false;
return processedProperties;
}
getSelectedMedia() {
const mediaElm = this.editor.selection.getNode();
if (!mediaElm) {
return null;
}
if (mediaElm.nodeName.toLowerCase() === 'video' || mediaElm.nodeName.toLowerCase() === 'audio') {
return mediaElm;
}
if (mediaElm.querySelector('video')) {
return mediaElm.querySelector('video');
}
if (mediaElm.querySelector('audio')) {
return mediaElm.querySelector('audio');
}
return null;
}
getMediumProperties() {
const boolAttr = (elem, attr) => {
// As explained in MDL-64175, some OS (like Ubuntu), are removing the value for these attributes.
// So in order to check if attr="true", we need to check if the attribute exists and if the value is empty or true.
return (elem.hasAttribute(attr) && (elem.getAttribute(attr) || elem.getAttribute(attr) === ''));
};
const tracks = {
subtitles: [],
captions: [],
descriptions: [],
chapters: [],
metadata: []
};
const sources = [];
const medium = this.selectedMedia;
if (!medium) {
return null;
}
medium.querySelectorAll('track').forEach((track) => {
tracks[track.getAttribute('kind')].push({
src: track.getAttribute('src'),
srclang: track.getAttribute('srclang'),
label: track.getAttribute('label'),
defaultTrack: boolAttr(track, 'default')
});
});
medium.querySelectorAll('source').forEach((source) => {
sources.push(source.src);
});
return {
type: medium.nodeName.toLowerCase() === 'video' ? Selectors.EMBED.mediaTypes.video : Selectors.EMBED.mediaTypes.audio,
sources,
poster: medium.getAttribute('poster'),
title: medium.getAttribute('title'),
width: medium.getAttribute('width'),
height: medium.getAttribute('height'),
autoplay: boolAttr(medium, 'autoplay'),
loop: boolAttr(medium, 'loop'),
muted: boolAttr(medium, 'muted'),
controls: boolAttr(medium, 'controls'),
tracks,
};
}
prepareMoodleLang() {
const moodleLangs = getMoodleLang(this.editor);
const currentLanguage = getCurrentLanguage(this.editor);
const installed = Object.entries(moodleLangs.installed).map(([lang, code]) => ({
lang,
code,
"default": lang === currentLanguage,
}));
const available = Object.entries(moodleLangs.available).map(([lang, code]) => ({
lang,
code,
"default": lang === currentLanguage,
}));
return {
installed,
available,
};
}
getMoodleLangObj(subtitleLang) {
const {available} = getMoodleLang(this.editor);
if (available[subtitleLang]) {
return {
lang: subtitleLang,
code: available[subtitleLang],
};
}
return null;
}
filePickerCallback(params, element, fpType) {
if (params.url !== '') {
const tabPane = element.closest('.tab-pane');
element.closest(Selectors.EMBED.elements.source).querySelector(Selectors.EMBED.elements.url).value = params.url;
if (tabPane.id === this.editor.getElement().id + '_' + Selectors.EMBED.mediaTypes.link.toLowerCase()) {
tabPane.querySelector(Selectors.EMBED.elements.name).value = params.file;
}
if (fpType === 'subtitle') {
// If the file is subtitle file. We need to match the language and label for that file.
const subtitleLang = params.file.split('.vtt')[0].split('-').slice(-1)[0];
const langObj = this.getMoodleLangObj(subtitleLang);
if (langObj) {
const track = element.closest(Selectors.EMBED.elements.track);
track.querySelector(Selectors.EMBED.elements.trackLabel).value = langObj.lang.trim();
track.querySelector(Selectors.EMBED.elements.trackLang).value = langObj.code;
}
}
}
}
addMediaSourceComponent(element, callback) {
const sourceElement = element.closest(Selectors.EMBED.elements.source + Selectors.EMBED.elements.mediaSource);
const clone = sourceElement.cloneNode(true);
sourceElement.querySelector('.removecomponent-wrapper').classList.remove('hidden');
sourceElement.querySelector('.addcomponent-wrapper').classList.add('hidden');
sourceElement.parentNode.insertBefore(clone, sourceElement.nextSibling);
if (callback) {
callback(clone);
}
}
removeMediaSourceComponent(element) {
const sourceElement = element.closest(Selectors.EMBED.elements.source + Selectors.EMBED.elements.mediaSource);
sourceElement.remove();
}
addTrackComponent(element, callback) {
const trackElement = element.closest(Selectors.EMBED.elements.track);
const clone = trackElement.cloneNode(true);
trackElement.querySelector('.removecomponent-wrapper').classList.remove('hidden');
trackElement.querySelector('.addcomponent-wrapper').classList.add('hidden');
trackElement.parentNode.insertBefore(clone, trackElement.nextSibling);
if (callback) {
callback(clone);
}
}
removeTrackComponent(element) {
const sourceElement = element.closest(Selectors.EMBED.elements.track);
sourceElement.remove();
}
getMediumTypeFromTabPane(tabPane) {
return tabPane.getAttribute('data-medium-type');
}
getTrackTypeFromTabPane(tabPane) {
return tabPane.getAttribute('data-track-kind');
}
getMediaHTML(form) {
const mediumType = this.getMediumTypeFromTabPane(form.querySelector('.root.tab-content > .tab-pane.active'));
const tabContent = form.querySelector(Selectors.EMBED.elements[mediumType.toLowerCase() + 'Pane']);
return this['getMediaHTML' + mediumType[0].toUpperCase() + mediumType.substr(1)](tabContent);
}
getMediaHTMLLink(tab) {
const context = {
url: tab.querySelector(Selectors.EMBED.elements.url).value,
name: tab.querySelector(Selectors.EMBED.elements.name).value || false
};
return context.url ? Templates.renderForPromise('tiny_mediacms/embed_media_link', context) : '';
}
getMediaHTMLVideo(tab) {
const context = this.getContextForMediaHTML(tab);
context.width = tab.querySelector(Selectors.EMBED.elements.width).value || false;
context.height = tab.querySelector(Selectors.EMBED.elements.height).value || false;
context.poster = tab.querySelector(
`${Selectors.EMBED.elements.posterSource} ${Selectors.EMBED.elements.url}`
).value || false;
return context.sources.length ? Templates.renderForPromise('tiny_mediacms/embed_media_video', context) : '';
}
getMediaHTMLAudio(tab) {
const context = this.getContextForMediaHTML(tab);
return context.sources.length ? Templates.renderForPromise('tiny_mediacms/embed_media_audio', context) : '';
}
getContextForMediaHTML(tab) {
const tracks = Array.from(tab.querySelectorAll(Selectors.EMBED.elements.track)).map(track => ({
track: track.querySelector(Selectors.EMBED.elements.trackSource + ' ' + Selectors.EMBED.elements.url).value,
kind: this.getTrackTypeFromTabPane(track.closest('.tab-pane')),
label: track.querySelector(Selectors.EMBED.elements.trackLabel).value ||
track.querySelector(Selectors.EMBED.elements.trackLang).value,
srclang: track.querySelector(Selectors.EMBED.elements.trackLang).value,
defaultTrack: track.querySelector(Selectors.EMBED.elements.trackDefault).checked ? "true" : null
})).filter((track) => !!track.track);
const sources = Array.from(tab.querySelectorAll(Selectors.EMBED.elements.mediaSource + ' '
+ Selectors.EMBED.elements.url))
.filter((source) => !!source.value)
.map((source) => source.value);
return {
sources,
description: tab.querySelector(Selectors.EMBED.elements.mediaSource + ' '
+ Selectors.EMBED.elements.url).value || false,
tracks,
showControls: tab.querySelector(Selectors.EMBED.elements.mediaControl).checked,
autoplay: tab.querySelector(Selectors.EMBED.elements.mediaAutoplay).checked,
muted: tab.querySelector(Selectors.EMBED.elements.mediaMute).checked,
loop: tab.querySelector(Selectors.EMBED.elements.mediaLoop).checked,
title: tab.querySelector(Selectors.EMBED.elements.title).value || false
};
}
getFilepickerTypeFromElement(element) {
if (element.closest(Selectors.EMBED.elements.posterSource)) {
return 'image';
}
if (element.closest(Selectors.EMBED.elements.trackSource)) {
return 'subtitle';
}
return 'media';
}
async clickHandler(e) {
const element = e.target;
const mediaBrowser = element.closest(Selectors.EMBED.actions.mediaBrowser);
if (mediaBrowser) {
e.preventDefault();
const fpType = this.getFilepickerTypeFromElement(element);
const params = await displayFilepicker(this.editor, fpType);
this.filePickerCallback(params, element, fpType);
}
const addComponentSourceAction = element.closest(Selectors.EMBED.elements.mediaSource + ' .addcomponent');
if (addComponentSourceAction) {
e.preventDefault();
this.addMediaSourceComponent(element);
}
const removeComponentSourceAction = element.closest(Selectors.EMBED.elements.mediaSource + ' .removecomponent');
if (removeComponentSourceAction) {
e.preventDefault();
this.removeMediaSourceComponent(element);
}
const addComponentTrackAction = element.closest(Selectors.EMBED.elements.track + ' .addcomponent');
if (addComponentTrackAction) {
e.preventDefault();
this.addTrackComponent(element);
}
const removeComponentTrackAction = element.closest(Selectors.EMBED.elements.track + ' .removecomponent');
if (removeComponentTrackAction) {
e.preventDefault();
this.removeTrackComponent(element);
}
// Only allow one track per tab to be selected as "default".
const trackDefaultAction = element.closest(Selectors.EMBED.elements.trackDefault);
if (trackDefaultAction && trackDefaultAction.checked) {
const getKind = (el) => this.getTrackTypeFromTabPane(el.parentElement.closest('.tab-pane'));
element.parentElement
.closest('.root.tab-content')
.querySelectorAll(Selectors.EMBED.elements.trackDefault)
.forEach((select) => {
if (select !== element && getKind(element) === getKind(select)) {
select.checked = false;
}
});
}
}
async handleDialogueSubmission(event, modal) {
const {html} = await this.getMediaHTML(modal.getRoot()[0]);
if (html) {
if (this.isUpdating) {
this.selectedMedia.outerHTML = html;
this.isUpdating = false;
} else {
this.editor.insertContent(html);
}
}
}
async registerEventListeners(modal) {
await modal.getBody();
const $root = modal.getRoot();
const root = $root[0];
if (this.canShowFilePicker || this.canShowFilePickerPoster || this.canShowFilePickerTrack) {
root.addEventListener('click', this.clickHandler.bind(this));
}
$root.on(ModalEvents.save, this.handleDialogueSubmission.bind(this));
$root.on(ModalEvents.hidden, () => {
this.currentModal.destroy();
});
$root.on(ModalEvents.shown, () => {
root.querySelectorAll(Selectors.EMBED.elements.trackLang).forEach((dropdown) => {
const defaultVal = dropdown.getAttribute('data-value');
if (defaultVal) {
dropdown.value = defaultVal;
}
});
});
}
}

View File

@@ -0,0 +1,47 @@
// 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/>.
/**
* Embedded Media Management Modal for Tiny.
*
* @module tiny_mediacms/embedmodal
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Modal from 'core/modal';
import {component} from './common';
export default class EmbedModal extends Modal {
static TYPE = `${component}/modal`;
static TEMPLATE = `${component}/embed_media_modal`;
registerEventListeners() {
// Call the parent registration.
super.registerEventListeners();
// Register to close on save/cancel.
this.registerCloseOnSave();
this.registerCloseOnCancel();
}
configure(modalConfig) {
modalConfig.large = true;
modalConfig.removeOnClose = true;
modalConfig.show = true;
super.configure(modalConfig);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,47 @@
// 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/>.
/**
* Iframe Embed Modal for Tiny Media2.
*
* @module tiny_mediacms/iframemodal
* @copyright 2024
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Modal from 'core/modal';
import {component} from './common';
export default class IframeModal extends Modal {
static TYPE = `${component}/iframemodal`;
static TEMPLATE = `${component}/iframe_embed_modal`;
registerEventListeners() {
// Call the parent registration.
super.registerEventListeners();
// Register to close on save/cancel.
this.registerCloseOnSave();
this.registerCloseOnCancel();
}
configure(modalConfig) {
modalConfig.large = true;
modalConfig.removeOnClose = true;
modalConfig.show = true;
super.configure(modalConfig);
}
}

View File

@@ -0,0 +1,273 @@
// 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 Image class for Moodle.
*
* @module tiny_mediacms/image
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Selectors from './selectors';
import ImageModal from './imagemodal';
import {getImagePermissions} from './options';
import {getFilePicker} from 'editor_tiny/options';
import {ImageInsert} from 'tiny_mediacms/imageinsert';
import {ImageDetails} from 'tiny_mediacms/imagedetails';
import {prefetchStrings} from 'core/prefetch';
import {getString} from 'core/str';
import {
bodyImageInsert,
footerImageInsert,
bodyImageDetails,
footerImageDetails,
showElements,
hideElements,
isPercentageValue,
} from 'tiny_mediacms/imagehelpers';
prefetchStrings('tiny_mediacms', [
'imageurlrequired',
'sizecustom_help',
]);
export default class MediaImage {
canShowFilePicker = false;
editor = null;
currentModal = null;
/**
* @type {HTMLElement|null} The root element.
*/
root = null;
constructor(editor) {
const permissions = getImagePermissions(editor);
const options = getFilePicker(editor, 'image');
// Indicates whether the file picker can be shown.
this.canShowFilePicker = permissions.filepicker
&& (typeof options !== 'undefined')
&& Object.keys(options.repositories).length > 0;
// Indicates whether the drop zone area can be shown.
this.canShowDropZone = (typeof options !== 'undefined') &&
Object.values(options.repositories).some(repository => repository.type === 'upload');
this.editor = editor;
}
async displayDialogue() {
const currentImageData = await this.getCurrentImageData();
this.currentModal = await ImageModal.create();
this.root = this.currentModal.getRoot()[0];
if (currentImageData && currentImageData.src) {
this.loadPreviewImage(currentImageData.src);
} else {
this.loadInsertImage();
}
}
/**
* Displays an insert image view asynchronously.
*
* @returns {Promise<void>}
*/
loadInsertImage = async function() {
const templateContext = {
elementid: this.editor.id,
showfilepicker: this.canShowFilePicker,
showdropzone: this.canShowDropZone,
};
Promise.all([bodyImageInsert(templateContext, this.root), footerImageInsert(templateContext, this.root)])
.then(() => {
const imageinsert = new ImageInsert(
this.root,
this.editor,
this.currentModal,
this.canShowFilePicker,
this.canShowDropZone,
);
imageinsert.init();
return;
})
.catch(error => {
window.console.log(error);
});
};
async getTemplateContext(data) {
return {
elementid: this.editor.id,
showfilepicker: this.canShowFilePicker,
...data,
};
}
async getCurrentImageData() {
const selectedImageProperties = this.getSelectedImageProperties();
if (!selectedImageProperties) {
return {};
}
const properties = {...selectedImageProperties};
if (properties.src) {
properties.haspreview = true;
}
if (!properties.alt) {
properties.presentation = true;
}
return properties;
}
/**
* Asynchronously loads and previews an image from the provided URL.
*
* @param {string} url - The URL of the image to load and preview.
* @returns {Promise<void>}
*/
loadPreviewImage = async function(url) {
this.startImageLoading();
const image = new Image();
image.src = url;
image.addEventListener('error', async() => {
const urlWarningLabelEle = this.root.querySelector(Selectors.IMAGE.elements.urlWarning);
urlWarningLabelEle.innerHTML = await getString('imageurlrequired', 'tiny_mediacms');
showElements(Selectors.IMAGE.elements.urlWarning, this.root);
this.stopImageLoading();
});
image.addEventListener('load', async() => {
const currentImageData = await this.getCurrentImageData();
let templateContext = await this.getTemplateContext(currentImageData);
templateContext.sizecustomhelpicon = {text: await getString('sizecustom_help', 'tiny_mediacms')};
Promise.all([bodyImageDetails(templateContext, this.root), footerImageDetails(templateContext, this.root)])
.then(() => {
this.stopImageLoading();
return;
})
.then(() => {
const imagedetails = new ImageDetails(
this.root,
this.editor,
this.currentModal,
this.canShowFilePicker,
this.canShowDropZone,
url,
image,
);
imagedetails.init();
return;
})
.catch(error => {
window.console.log(error);
});
});
};
getSelectedImageProperties() {
const image = this.getSelectedImage();
if (!image) {
this.selectedImage = null;
return null;
}
const properties = {
src: null,
alt: null,
width: null,
height: null,
presentation: false,
customStyle: '', // Custom CSS styles applied to the image.
};
const getImageHeight = (image) => {
if (!isPercentageValue(String(image.height))) {
return parseInt(image.height, 10);
}
return image.height;
};
const getImageWidth = (image) => {
if (!isPercentageValue(String(image.width))) {
return parseInt(image.width, 10);
}
return image.width;
};
// Get the current selection.
this.selectedImage = image;
properties.customStyle = image.style.cssText;
const width = getImageWidth(image);
if (width !== 0) {
properties.width = width;
}
const height = getImageHeight(image);
if (height !== 0) {
properties.height = height;
}
properties.src = image.getAttribute('src');
properties.alt = image.getAttribute('alt') || '';
properties.presentation = (image.getAttribute('role') === 'presentation');
return properties;
}
getSelectedImage() {
const imgElm = this.editor.selection.getNode();
const figureElm = this.editor.dom.getParent(imgElm, 'figure.image');
if (figureElm) {
return this.editor.dom.select('img', figureElm)[0];
}
if (imgElm && (imgElm.nodeName.toUpperCase() !== 'IMG' || this.isPlaceholderImage(imgElm))) {
return null;
}
return imgElm;
}
isPlaceholderImage(imgElm) {
if (imgElm.nodeName.toUpperCase() !== 'IMG') {
return false;
}
return (imgElm.hasAttribute('data-mce-object') || imgElm.hasAttribute('data-mce-placeholder'));
}
/**
* Displays the upload loader and disables UI elements while loading a file.
*/
startImageLoading() {
showElements(Selectors.IMAGE.elements.loaderIcon, this.root);
hideElements(Selectors.IMAGE.elements.insertImage, this.root);
}
/**
* Displays the upload loader and disables UI elements while loading a file.
*/
stopImageLoading() {
hideElements(Selectors.IMAGE.elements.loaderIcon, this.root);
showElements(Selectors.IMAGE.elements.insertImage, this.root);
}
}

View File

@@ -0,0 +1,614 @@
// 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 image details class for Moodle.
*
* @module tiny_mediacms/imagedetails
* @copyright 2024 Meirza <meirza.arson@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Config from 'core/config';
import ModalEvents from 'core/modal_events';
import Notification from 'core/notification';
import Pending from 'core/pending';
import Selectors from './selectors';
import Templates from 'core/templates';
import {getString} from 'core/str';
import {ImageInsert} from 'tiny_mediacms/imageinsert';
import {
bodyImageInsert,
footerImageInsert,
showElements,
hideElements,
isPercentageValue,
} from 'tiny_mediacms/imagehelpers';
export class ImageDetails {
DEFAULTS = {
WIDTH: 160,
HEIGHT: 160,
};
rawImageDimensions = null;
constructor(
root,
editor,
currentModal,
canShowFilePicker,
canShowDropZone,
currentUrl,
image,
) {
this.root = root;
this.editor = editor;
this.currentModal = currentModal;
this.canShowFilePicker = canShowFilePicker;
this.canShowDropZone = canShowDropZone;
this.currentUrl = currentUrl;
this.image = image;
}
init = function() {
this.currentModal.setTitle(getString('imagedetails', 'tiny_mediacms'));
this.imageTypeChecked();
this.presentationChanged();
this.storeImageDimensions(this.image);
this.setImageDimensions();
this.registerEventListeners();
};
/**
* Loads and displays a preview image based on the provided URL, and handles image loading events.
*/
loadInsertImage = async function() {
const templateContext = {
elementid: this.editor.id,
showfilepicker: this.canShowFilePicker,
showdropzone: this.canShowDropZone,
};
Promise.all([bodyImageInsert(templateContext, this.root), footerImageInsert(templateContext, this.root)])
.then(() => {
const imageinsert = new ImageInsert(
this.root,
this.editor,
this.currentModal,
this.canShowFilePicker,
this.canShowDropZone,
);
imageinsert.init();
return;
})
.catch(error => {
window.console.log(error);
});
};
storeImageDimensions(image) {
// Store dimensions of the raw image, falling back to defaults for images without dimensions (e.g. SVG).
this.rawImageDimensions = {
width: image.width || this.DEFAULTS.WIDTH,
height: image.height || this.DEFAULTS.HEIGHT,
};
const getCurrentWidth = (element) => {
if (element.value === '') {
element.value = this.rawImageDimensions.width;
}
return element.value;
};
const getCurrentHeight = (element) => {
if (element.value === '') {
element.value = this.rawImageDimensions.height;
}
return element.value;
};
const widthInput = this.root.querySelector(Selectors.IMAGE.elements.width);
const currentWidth = getCurrentWidth(widthInput);
const heightInput = this.root.querySelector(Selectors.IMAGE.elements.height);
const currentHeight = getCurrentHeight(heightInput);
const preview = this.root.querySelector(Selectors.IMAGE.elements.preview);
preview.setAttribute('src', image.src);
preview.style.display = '';
// Ensure the checkbox always in unchecked status when an image loads at first.
const constrain = this.root.querySelector(Selectors.IMAGE.elements.constrain);
if (isPercentageValue(currentWidth) && isPercentageValue(currentHeight)) {
constrain.checked = currentWidth === currentHeight;
} else if (image.width === 0 || image.height === 0) {
// If we don't have both dimensions of the image, we can't auto-size it, so disable control.
constrain.disabled = 'disabled';
} else {
// This is the same as comparing to 3 decimal places.
const widthRatio = Math.round(100 * parseInt(currentWidth, 10) / image.width);
const heightRatio = Math.round(100 * parseInt(currentHeight, 10) / image.height);
constrain.checked = widthRatio === heightRatio;
}
/**
* Sets the selected size option based on current width and height values.
*
* @param {number} currentWidth - The current width value.
* @param {number} currentHeight - The current height value.
*/
const setSelectedSize = (currentWidth, currentHeight) => {
if (this.rawImageDimensions.width === currentWidth &&
this.rawImageDimensions.height === currentHeight
) {
this.currentWidth = this.rawImageDimensions.width;
this.currentHeight = this.rawImageDimensions.height;
this.sizeChecked('original');
} else {
this.currentWidth = currentWidth;
this.currentHeight = currentHeight;
this.sizeChecked('custom');
}
};
setSelectedSize(Number(currentWidth), Number(currentHeight));
}
/**
* Handles the selection of image size options and updates the form inputs accordingly.
*
* @param {string} option - The selected image size option ("original" or "custom").
*/
sizeChecked(option) {
const widthInput = this.root.querySelector(Selectors.IMAGE.elements.width);
const heightInput = this.root.querySelector(Selectors.IMAGE.elements.height);
if (option === "original") {
this.sizeOriginalChecked();
widthInput.value = this.rawImageDimensions.width;
heightInput.value = this.rawImageDimensions.height;
} else if (option === "custom") {
this.sizeCustomChecked();
widthInput.value = this.currentWidth;
heightInput.value = this.currentHeight;
// If the current size is equal to the original size, then check the Keep proportion checkbox.
if (this.currentWidth === this.rawImageDimensions.width && this.currentHeight === this.rawImageDimensions.height) {
const constrainField = this.root.querySelector(Selectors.IMAGE.elements.constrain);
constrainField.checked = true;
}
}
this.autoAdjustSize();
}
autoAdjustSize(forceHeight = false) {
// If we do not know the image size, do not do anything.
if (!this.rawImageDimensions) {
return;
}
const widthField = this.root.querySelector(Selectors.IMAGE.elements.width);
const heightField = this.root.querySelector(Selectors.IMAGE.elements.height);
const normalizeFieldData = (fieldData) => {
fieldData.isPercentageValue = !!isPercentageValue(fieldData.field.value);
if (fieldData.isPercentageValue) {
fieldData.percentValue = parseInt(fieldData.field.value, 10);
fieldData.pixelSize = this.rawImageDimensions[fieldData.type] / 100 * fieldData.percentValue;
} else {
fieldData.pixelSize = parseInt(fieldData.field.value, 10);
fieldData.percentValue = fieldData.pixelSize / this.rawImageDimensions[fieldData.type] * 100;
}
return fieldData;
};
const getKeyField = () => {
const getValue = () => {
if (forceHeight) {
return {
field: heightField,
type: 'height',
};
} else {
return {
field: widthField,
type: 'width',
};
}
};
const currentValue = getValue();
if (currentValue.field.value === '') {
currentValue.field.value = this.rawImageDimensions[currentValue.type];
}
return normalizeFieldData(currentValue);
};
const getRelativeField = () => {
if (forceHeight) {
return normalizeFieldData({
field: widthField,
type: 'width',
});
} else {
return normalizeFieldData({
field: heightField,
type: 'height',
});
}
};
// Now update with the new values.
const constrainField = this.root.querySelector(Selectors.IMAGE.elements.constrain);
if (constrainField.checked) {
const keyField = getKeyField();
const relativeField = getRelativeField();
// We are keeping the image in proportion.
// Calculate the size for the relative field.
if (keyField.isPercentageValue) {
// In proportion, so the percentages are the same.
relativeField.field.value = keyField.field.value;
relativeField.percentValue = keyField.percentValue;
} else {
relativeField.pixelSize = Math.round(
keyField.pixelSize / this.rawImageDimensions[keyField.type] * this.rawImageDimensions[relativeField.type]
);
relativeField.field.value = relativeField.pixelSize;
}
}
// Store the custom width and height to reuse.
this.currentWidth = Number(widthField.value) !== this.rawImageDimensions.width ? widthField.value : this.currentWidth;
this.currentHeight = Number(heightField.value) !== this.rawImageDimensions.height ? heightField.value : this.currentHeight;
}
/**
* Sets the dimensions of the image preview element based on user input and constraints.
*/
setImageDimensions = () => {
const imagePreviewBox = this.root.querySelector(Selectors.IMAGE.elements.previewBox);
const image = this.root.querySelector(Selectors.IMAGE.elements.preview);
const widthField = this.root.querySelector(Selectors.IMAGE.elements.width);
const heightField = this.root.querySelector(Selectors.IMAGE.elements.height);
const updateImageDimensions = () => {
// Get the latest dimensions of the preview box for responsiveness.
const boxWidth = imagePreviewBox.clientWidth;
const boxHeight = imagePreviewBox.clientHeight;
// Get the new width and height for the image.
const dimensions = this.fitSquareIntoBox(widthField.value, heightField.value, boxWidth, boxHeight);
image.style.width = `${dimensions.width}px`;
image.style.height = `${dimensions.height}px`;
};
// If the client size is zero, then get the new dimensions once the modal is shown.
if (imagePreviewBox.clientWidth === 0) {
// Call the shown event.
this.currentModal.getRoot().on(ModalEvents.shown, () => {
updateImageDimensions();
});
} else {
updateImageDimensions();
}
};
/**
* Handles the selection of the "Original Size" option and updates the form elements accordingly.
*/
sizeOriginalChecked() {
this.root.querySelector(Selectors.IMAGE.elements.sizeOriginal).checked = true;
this.root.querySelector(Selectors.IMAGE.elements.sizeCustom).checked = false;
hideElements(Selectors.IMAGE.elements.properties, this.root);
}
/**
* Handles the selection of the "Custom Size" option and updates the form elements accordingly.
*/
sizeCustomChecked() {
this.root.querySelector(Selectors.IMAGE.elements.sizeOriginal).checked = false;
this.root.querySelector(Selectors.IMAGE.elements.sizeCustom).checked = true;
showElements(Selectors.IMAGE.elements.properties, this.root);
}
/**
* Handles changes in the image presentation checkbox and enables/disables the image alt text input accordingly.
*/
presentationChanged() {
const presentation = this.root.querySelector(Selectors.IMAGE.elements.presentation);
const alt = this.root.querySelector(Selectors.IMAGE.elements.alt);
alt.disabled = presentation.checked;
// Counting the image description characters.
this.handleKeyupCharacterCount();
}
/**
* This function checks whether an image URL is local (within the same website's domain) or external (from an external source).
* Depending on the result, it dynamically updates the visibility and content of HTML elements in a user interface.
* If the image is local then we only show it's filename.
* If the image is external then it will show full URL and it can be updated.
*/
imageTypeChecked() {
const regex = new RegExp(`${Config.wwwroot}`);
// True if the URL is from external, otherwise false.
const isExternalUrl = regex.test(this.currentUrl) === false;
// Hide the URL input.
hideElements(Selectors.IMAGE.elements.url, this.root);
if (!isExternalUrl) {
// Split the URL by '/' to get an array of segments.
const segments = this.currentUrl.split('/');
// Get the last segment, which should be the filename.
const filename = segments.pop().split('?')[0];
// Show the file name.
this.setFilenameLabel(decodeURI(filename));
} else {
this.setFilenameLabel(decodeURI(this.currentUrl));
}
}
/**
* Set the string for the URL label element.
*
* @param {string} label - The label text to set.
*/
setFilenameLabel(label) {
const urlLabelEle = this.root.querySelector(Selectors.IMAGE.elements.fileNameLabel);
if (urlLabelEle) {
urlLabelEle.innerHTML = label;
urlLabelEle.setAttribute("title", label);
}
}
toggleAriaInvalid(selectors, predicate) {
selectors.forEach((selector) => {
const elements = this.root.querySelectorAll(selector);
elements.forEach((element) => element.setAttribute('aria-invalid', predicate));
});
}
hasErrorUrlField() {
const urlError = this.currentUrl === '';
if (urlError) {
showElements(Selectors.IMAGE.elements.urlWarning, this.root);
} else {
hideElements(Selectors.IMAGE.elements.urlWarning, this.root);
}
this.toggleAriaInvalid([Selectors.IMAGE.elements.url], urlError);
return urlError;
}
hasErrorAltField() {
const alt = this.root.querySelector(Selectors.IMAGE.elements.alt).value;
const presentation = this.root.querySelector(Selectors.IMAGE.elements.presentation).checked;
const imageAltError = alt === '' && !presentation;
if (imageAltError) {
showElements(Selectors.IMAGE.elements.altWarning, this.root);
} else {
hideElements(Selectors.IMAGE.elements.urlWaaltWarningrning, this.root);
}
this.toggleAriaInvalid([Selectors.IMAGE.elements.alt, Selectors.IMAGE.elements.presentation], imageAltError);
return imageAltError;
}
updateWarning() {
const urlError = this.hasErrorUrlField();
const imageAltError = this.hasErrorAltField();
return urlError || imageAltError;
}
getImageContext() {
// Check if there are any accessibility issues.
if (this.updateWarning()) {
return null;
}
const classList = [];
const constrain = this.root.querySelector(Selectors.IMAGE.elements.constrain).checked;
const sizeOriginal = this.root.querySelector(Selectors.IMAGE.elements.sizeOriginal).checked;
if (constrain || sizeOriginal) {
// If the Auto size checkbox is checked or the Original size is checked, then apply the responsive class.
classList.push(Selectors.IMAGE.styles.responsive);
} else {
// Otherwise, remove it.
classList.pop(Selectors.IMAGE.styles.responsive);
}
return {
url: this.currentUrl,
alt: this.root.querySelector(Selectors.IMAGE.elements.alt).value,
width: this.root.querySelector(Selectors.IMAGE.elements.width).value,
height: this.root.querySelector(Selectors.IMAGE.elements.height).value,
presentation: this.root.querySelector(Selectors.IMAGE.elements.presentation).checked,
customStyle: this.root.querySelector(Selectors.IMAGE.elements.customStyle).value,
classlist: classList.join(' '),
};
}
setImage() {
const pendingPromise = new Pending('tiny_mediacms:setImage');
const url = this.currentUrl;
if (url === '') {
return;
}
// Check if there are any accessibility issues.
if (this.updateWarning()) {
pendingPromise.resolve();
return;
}
// Check for invalid width or height.
const width = this.root.querySelector(Selectors.IMAGE.elements.width).value;
if (!isPercentageValue(width) && isNaN(parseInt(width, 10))) {
this.root.querySelector(Selectors.IMAGE.elements.width).focus();
pendingPromise.resolve();
return;
}
const height = this.root.querySelector(Selectors.IMAGE.elements.height).value;
if (!isPercentageValue(height) && isNaN(parseInt(height, 10))) {
this.root.querySelector(Selectors.IMAGE.elements.height).focus();
pendingPromise.resolve();
return;
}
Templates.render('tiny_mediacms/image', this.getImageContext())
.then((html) => {
this.editor.insertContent(html);
this.currentModal.destroy();
pendingPromise.resolve();
return html;
})
.catch(error => {
window.console.log(error);
});
}
/**
* Deletes the image after confirming with the user and loads the insert image page.
*/
deleteImage() {
Notification.deleteCancelPromise(
getString('deleteimage', 'tiny_mediacms'),
getString('deleteimagewarning', 'tiny_mediacms'),
).then(() => {
hideElements(Selectors.IMAGE.elements.altWarning, this.root);
// Removing the image in the preview will bring the user to the insert page.
this.loadInsertImage();
return;
}).catch(error => {
window.console.log(error);
});
}
registerEventListeners() {
const submitAction = this.root.querySelector(Selectors.IMAGE.actions.submit);
submitAction.addEventListener('click', (e) => {
e.preventDefault();
this.setImage();
});
const deleteImageEle = this.root.querySelector(Selectors.IMAGE.actions.deleteImage);
deleteImageEle.addEventListener('click', () => {
this.deleteImage();
});
deleteImageEle.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
this.deleteImage();
}
});
this.root.addEventListener('change', (e) => {
const presentationEle = e.target.closest(Selectors.IMAGE.elements.presentation);
if (presentationEle) {
this.presentationChanged();
}
const constrainEle = e.target.closest(Selectors.IMAGE.elements.constrain);
if (constrainEle) {
this.autoAdjustSize();
}
const sizeOriginalEle = e.target.closest(Selectors.IMAGE.elements.sizeOriginal);
if (sizeOriginalEle) {
this.sizeChecked('original');
}
const sizeCustomEle = e.target.closest(Selectors.IMAGE.elements.sizeCustom);
if (sizeCustomEle) {
this.sizeChecked('custom');
}
});
this.root.addEventListener('blur', (e) => {
if (e.target.nodeType === Node.ELEMENT_NODE) {
const presentationEle = e.target.closest(Selectors.IMAGE.elements.presentation);
if (presentationEle) {
this.presentationChanged();
}
}
}, true);
// Character count.
this.root.addEventListener('keyup', (e) => {
const altEle = e.target.closest(Selectors.IMAGE.elements.alt);
if (altEle) {
this.handleKeyupCharacterCount();
}
});
this.root.addEventListener('input', (e) => {
const widthEle = e.target.closest(Selectors.IMAGE.elements.width);
if (widthEle) {
// Avoid empty value.
widthEle.value = widthEle.value === "" ? 0 : Number(widthEle.value);
this.autoAdjustSize();
}
const heightEle = e.target.closest(Selectors.IMAGE.elements.height);
if (heightEle) {
// Avoid empty value.
heightEle.value = heightEle.value === "" ? 0 : Number(heightEle.value);
this.autoAdjustSize(true);
}
});
}
handleKeyupCharacterCount() {
const alt = this.root.querySelector(Selectors.IMAGE.elements.alt).value;
const current = this.root.querySelector('#currentcount');
current.innerHTML = alt.length;
}
/**
* Calculates the dimensions to fit a square into a specified box while maintaining aspect ratio.
*
* @param {number} squareWidth - The width of the square.
* @param {number} squareHeight - The height of the square.
* @param {number} boxWidth - The width of the box.
* @param {number} boxHeight - The height of the box.
* @returns {Object} An object with the new width and height of the square to fit in the box.
*/
fitSquareIntoBox = (squareWidth, squareHeight, boxWidth, boxHeight) => {
if (squareWidth < boxWidth && squareHeight < boxHeight) {
// If the square is smaller than the box, keep its dimensions.
return {
width: squareWidth,
height: squareHeight,
};
}
// Calculate the scaling factor based on the minimum scaling required to fit in the box.
const widthScaleFactor = boxWidth / squareWidth;
const heightScaleFactor = boxHeight / squareHeight;
const minScaleFactor = Math.min(widthScaleFactor, heightScaleFactor);
// Scale the square's dimensions based on the aspect ratio and the minimum scaling factor.
const newWidth = squareWidth * minScaleFactor;
const newHeight = squareHeight * minScaleFactor;
return {
width: newWidth,
height: newHeight,
};
};
}

View File

@@ -0,0 +1,149 @@
// 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 image helpers.
*
* @module tiny_mediacms/imagehelpers
* @copyright 2024 Meirza <meirza.arson@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Templates from 'core/templates';
/**
* Renders and inserts the body template for inserting an image into the modal.
*
* @param {object} templateContext - The context for rendering the template.
* @param {HTMLElement} root - The root element where the template will be inserted.
* @returns {Promise<void>}
*/
export const bodyImageInsert = async(templateContext, root) => {
return Templates.renderForPromise('tiny_mediacms/insert_image_modal_insert', {...templateContext})
.then(({html, js}) => {
Templates.replaceNodeContents(root.querySelector('.tiny_imagecms_body_template'), html, js);
return;
})
.catch(error => {
window.console.log(error);
});
};
/**
* Renders and inserts the footer template for inserting an image into the modal.
*
* @param {object} templateContext - The context for rendering the template.
* @param {HTMLElement} root - The root element where the template will be inserted.
* @returns {Promise<void>}
*/
export const footerImageInsert = async(templateContext, root) => {
return Templates.renderForPromise('tiny_mediacms/insert_image_modal_insert_footer', {...templateContext})
.then(({html, js}) => {
Templates.replaceNodeContents(root.querySelector('.tiny_imagecms_footer_template'), html, js);
return;
})
.catch(error => {
window.console.log(error);
});
};
/**
* Renders and inserts the body template for displaying image details in the modal.
*
* @param {object} templateContext - The context for rendering the template.
* @param {HTMLElement} root - The root element where the template will be inserted.
* @returns {Promise<void>}
*/
export const bodyImageDetails = async(templateContext, root) => {
return Templates.renderForPromise('tiny_mediacms/insert_image_modal_details', {...templateContext})
.then(({html, js}) => {
Templates.replaceNodeContents(root.querySelector('.tiny_imagecms_body_template'), html, js);
return;
})
.catch(error => {
window.console.log(error);
});
};
/**
* Renders and inserts the footer template for displaying image details in the modal.
* @param {object} templateContext - The context for rendering the template.
* @param {HTMLElement} root - The root element where the template will be inserted.
* @returns {Promise<void>}
*/
export const footerImageDetails = async(templateContext, root) => {
return Templates.renderForPromise('tiny_mediacms/insert_image_modal_details_footer', {...templateContext})
.then(({html, js}) => {
Templates.replaceNodeContents(root.querySelector('.tiny_imagecms_footer_template'), html, js);
return;
})
.catch(error => {
window.console.log(error);
});
};
/**
* Show the element(s).
*
* @param {string|string[]} elements - The CSS selector for the elements to toggle.
* @param {object} root - The CSS selector for the elements to toggle.
*/
export const showElements = (elements, root) => {
if (elements instanceof Array) {
elements.forEach((elementSelector) => {
const element = root.querySelector(elementSelector);
if (element) {
element.classList.remove('d-none');
}
});
} else {
const element = root.querySelector(elements);
if (element) {
element.classList.remove('d-none');
}
}
};
/**
* Hide the element(s).
*
* @param {string|string[]} elements - The CSS selector for the elements to toggle.
* @param {object} root - The CSS selector for the elements to toggle.
*/
export const hideElements = (elements, root) => {
if (elements instanceof Array) {
elements.forEach((elementSelector) => {
const element = root.querySelector(elementSelector);
if (element) {
element.classList.add('d-none');
}
});
} else {
const element = root.querySelector(elements);
if (element) {
element.classList.add('d-none');
}
}
};
/**
* Checks if the given value is a percentage value.
*
* @param {string} value - The value to check.
* @returns {boolean} True if the value is a percentage value, false otherwise.
*/
export const isPercentageValue = (value) => {
return value.match(/\d+%/);
};

View File

@@ -0,0 +1,282 @@
// 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 image insertion class for Moodle.
*
* @module tiny_mediacms/imageinsert
* @copyright 2024 Meirza <meirza.arson@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Selectors from './selectors';
import Dropzone from 'core/dropzone';
import uploadFile from 'editor_tiny/uploader';
import {prefetchStrings} from 'core/prefetch';
import {getStrings} from 'core/str';
import {component} from "./common";
import {getFilePicker} from 'editor_tiny/options';
import {displayFilepicker} from 'editor_tiny/utils';
import {ImageDetails} from 'tiny_mediacms/imagedetails';
import {
showElements,
hideElements,
bodyImageDetails,
footerImageDetails,
} from 'tiny_mediacms/imagehelpers';
prefetchStrings('tiny_mediacms', [
'insertimage',
'enterurl',
'enterurlor',
'imageurlrequired',
'uploading',
'loading',
'addfilesdrop',
'sizecustom_help',
]);
export class ImageInsert {
constructor(
root,
editor,
currentModal,
canShowFilePicker,
canShowDropZone,
) {
this.root = root;
this.editor = editor;
this.currentModal = currentModal;
this.canShowFilePicker = canShowFilePicker;
this.canShowDropZone = canShowDropZone;
}
init = async function() {
// Get the localization lang strings and turn them into object.
const langStringKeys = [
'insertimage',
'enterurl',
'enterurlor',
'imageurlrequired',
'uploading',
'loading',
'addfilesdrop',
'sizecustom_help',
];
const langStringvalues = await getStrings([...langStringKeys].map((key) => ({key, component})));
// Convert array to object.
this.langStrings = Object.fromEntries(langStringKeys.map((key, index) => [key, langStringvalues[index]]));
this.currentModal.setTitle(this.langStrings.insertimage);
if (this.canShowDropZone) {
const dropZoneEle = document.querySelector(Selectors.IMAGE.elements.dropzoneContainer);
// Accepted types can be either a string or an array.
let acceptedTypes = getFilePicker(this.editor, 'image').accepted_types;
if (Array.isArray(acceptedTypes)) {
acceptedTypes = acceptedTypes.join(',');
}
const dropZone = new Dropzone(
dropZoneEle,
acceptedTypes,
files => {
this.handleUploadedFile(files);
}
);
dropZone.setLabel(this.langStrings.addfilesdrop);
dropZone.init();
}
await this.registerEventListeners();
};
/**
* Enables or disables the URL-related buttons in the footer based on the current URL and input value.
*/
toggleUrlButton() {
const urlInput = this.root.querySelector(Selectors.IMAGE.elements.url);
const url = urlInput.value;
const addUrl = this.root.querySelector(Selectors.IMAGE.actions.addUrl);
addUrl.disabled = !(url !== "" && this.isValidUrl(url));
}
/**
* Check if given string is a valid URL.
*
* @param {String} urlString URL the link will point to.
* @returns {boolean} True is valid, otherwise false.
*/
isValidUrl = urlString => {
const urlPattern = new RegExp('^(https?:\\/\\/)?' + // Protocol.
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // Domain name.
'((\\d{1,3}\\.){3}\\d{1,3})|localhost)' + // OR ip (v4) address, localhost.
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'); // Port and path.
return !!urlPattern.test(urlString);
};
/**
* Handles changes in the image URL input field and loads a preview of the image if the URL has changed.
*/
urlChanged() {
hideElements(Selectors.IMAGE.elements.urlWarning, this.root);
const input = this.root.querySelector(Selectors.IMAGE.elements.url);
if (input.value && input.value !== this.currentUrl) {
this.loadPreviewImage(input.value);
}
}
/**
* Loads and displays a preview image based on the provided URL, and handles image loading events.
*
* @param {string} url - The URL of the image to load and display.
*/
loadPreviewImage = function(url) {
this.startImageLoading();
this.currentUrl = url;
const image = new Image();
image.src = url;
image.addEventListener('error', () => {
const urlWarningLabelEle = this.root.querySelector(Selectors.IMAGE.elements.urlWarning);
urlWarningLabelEle.innerHTML = this.langStrings.imageurlrequired;
showElements(Selectors.IMAGE.elements.urlWarning, this.root);
this.currentUrl = "";
this.stopImageLoading();
});
image.addEventListener('load', () => {
let templateContext = {};
templateContext.sizecustomhelpicon = {text: this.langStrings.sizecustom_help};
Promise.all([bodyImageDetails(templateContext, this.root), footerImageDetails(templateContext, this.root)])
.then(() => {
const imagedetails = new ImageDetails(
this.root,
this.editor,
this.currentModal,
this.canShowFilePicker,
this.canShowDropZone,
this.currentUrl,
image,
);
imagedetails.init();
return;
}).then(() => {
this.stopImageLoading();
return;
})
.catch(error => {
window.console.log(error);
});
});
};
/**
* Displays the upload loader and disables UI elements while loading a file.
*/
startImageLoading() {
showElements(Selectors.IMAGE.elements.loaderIcon, this.root);
const elementsToHide = [
Selectors.IMAGE.elements.insertImage,
Selectors.IMAGE.elements.urlWarning,
Selectors.IMAGE.elements.modalFooter,
];
hideElements(elementsToHide, this.root);
}
/**
* Displays the upload loader and disables UI elements while loading a file.
*/
stopImageLoading() {
hideElements(Selectors.IMAGE.elements.loaderIcon, this.root);
const elementsToShow = [
Selectors.IMAGE.elements.insertImage,
Selectors.IMAGE.elements.modalFooter,
];
showElements(elementsToShow, this.root);
}
filePickerCallback(params) {
if (params.url) {
this.loadPreviewImage(params.url);
}
}
/**
* Updates the content of the loader icon.
*
* @param {HTMLElement} root - The root element containing the loader icon.
* @param {object} langStrings - An object containing language strings.
* @param {number|null} progress - The progress percentage (optional).
* @returns {void}
*/
updateLoaderIcon = (root, langStrings, progress = null) => {
const loaderIcon = root.querySelector(Selectors.IMAGE.elements.loaderIconContainer + ' div');
loaderIcon.innerHTML = progress !== null ? `${langStrings.uploading} ${Math.round(progress)}%` : langStrings.loading;
};
/**
* Handles the uploaded file, initiates the upload process, and updates the UI during the upload.
*
* @param {FileList} files - The list of files to upload (usually from a file input field).
* @returns {Promise<void>} A promise that resolves when the file is uploaded and processed.
*/
handleUploadedFile = async(files) => {
try {
this.startImageLoading();
const fileURL = await uploadFile(this.editor, 'image', files[0], files[0].name, (progress) => {
this.updateLoaderIcon(this.root, this.langStrings, progress);
});
// Set the loader icon content to "loading" after the file upload completes.
this.updateLoaderIcon(this.root, this.langStrings);
this.filePickerCallback({url: fileURL});
} catch (error) {
// Handle the error.
const urlWarningLabelEle = this.root.querySelector(Selectors.IMAGE.elements.urlWarning);
urlWarningLabelEle.innerHTML = error.error !== undefined ? error.error : error;
showElements(Selectors.IMAGE.elements.urlWarning, this.root);
this.stopImageLoading();
}
};
registerEventListeners() {
this.root.addEventListener('click', async(e) => {
const addUrlEle = e.target.closest(Selectors.IMAGE.actions.addUrl);
if (addUrlEle) {
this.urlChanged();
}
const imageBrowserAction = e.target.closest(Selectors.IMAGE.actions.imageBrowser);
if (imageBrowserAction && this.canShowFilePicker) {
e.preventDefault();
const params = await displayFilepicker(this.editor, 'image');
this.filePickerCallback(params);
}
});
this.root.addEventListener('input', (e) => {
const urlEle = e.target.closest(Selectors.IMAGE.elements.url);
if (urlEle) {
this.toggleUrlButton();
}
});
const fileInput = this.root.querySelector(Selectors.IMAGE.elements.fileInput);
if (fileInput) {
fileInput.addEventListener('change', () => {
this.handleUploadedFile(fileInput.files);
});
}
}
}

View File

@@ -0,0 +1,49 @@
// 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/>.
/**
* Image Modal for Tiny.
*
* @module tiny_mediacms/imagemodal
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Modal from 'core/modal';
import {component} from './common';
export default class ImageModal extends Modal {
static TYPE = `${component}/imagemodal`;
static TEMPLATE = `${component}/insert_image_modal`;
registerEventListeners() {
// Call the parent registration.
super.registerEventListeners();
// Register to close on save/cancel.
this.registerCloseOnSave();
this.registerCloseOnCancel();
}
configure(modalConfig) {
modalConfig.large = true;
modalConfig.removeOnClose = true;
modalConfig.show = true;
super.configure(modalConfig);
}
}
ImageModal.registerModalType();

View File

@@ -0,0 +1,86 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Tiny Media Manager plugin class for Moodle.
*
* @module tiny_mediacms/manager
* @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Templates from 'core/templates';
import {getString} from 'core/str';
import Modal from 'core/modal';
import * as ModalEvents from 'core/modal_events';
import {getData} from './options';
import Config from 'core/config';
export default class MediaManager {
editor = null;
area = null;
constructor(editor) {
this.editor = editor;
const data = getData(editor);
this.area = data.params.area;
this.area.itemid = data.fpoptions.image.itemid;
}
async displayDialogue() {
const modal = await Modal.create({
large: true,
title: getString('mediamanagerproperties', 'tiny_mediacms'),
body: Templates.render('tiny_mediacms/mm2_iframe', {
src: this.getIframeURL()
}),
removeOnClose: true,
show: true,
});
modal.getRoot().on(ModalEvents.bodyRendered, () => {
this.selectFirstElement();
});
document.querySelector('.modal-lg').style.cssText = `max-width: 850px`;
return modal;
}
// It will select the first element in the file manager.
selectFirstElement() {
const iframe = document.getElementById('mm2-iframe');
iframe.addEventListener('load', function() {
let intervalId = setInterval(function() {
const iDocument = iframe.contentWindow.document;
if (iDocument.querySelector('.filemanager')) {
const firstFocusableElement = iDocument.querySelector('.fp-navbar a:not([disabled])');
if (firstFocusableElement) {
firstFocusableElement.focus();
}
clearInterval(intervalId);
}
}, 200);
});
}
getIframeURL() {
const url = new URL(`${Config.wwwroot}/lib/editor/tiny/plugins/mediacms/manage.php`);
url.searchParams.append('elementid', this.editor.getElement().id);
for (const key in this.area) {
url.searchParams.append(key, this.area[key]);
}
return url.toString();
}
}

View File

@@ -0,0 +1,117 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Options helper for Tiny Media plugin.
*
* @module tiny_mediacms/options
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {getPluginOptionName} from 'editor_tiny/options';
import {pluginName} from './common';
const dataName = getPluginOptionName(pluginName, 'data');
const permissionsName = getPluginOptionName(pluginName, 'permissions');
const ltiName = getPluginOptionName(pluginName, 'lti');
/**
* Register the options for the Tiny Media plugin.
*
* @param {TinyMCE} editor
*/
export const register = (editor) => {
const registerOption = editor.options.register;
registerOption(permissionsName, {
processor: 'object',
"default": {
image: {
filepicker: false,
}
},
});
registerOption(dataName, {
processor: 'object',
"default": {
// MediaCMS video library configuration
mediacmsApiUrl: '', // e.g., 'https://deic.mediacms.io/api/v1/media'
mediacmsBaseUrl: '', // e.g., 'https://deic.mediacms.io'
mediacmsPageSize: 12,
// Auto-conversion settings
autoConvertEnabled: true, // Enable/disable auto-conversion of pasted MediaCMS URLs
autoConvertBaseUrl: '', // Base URL to restrict auto-conversion (empty = allow all MediaCMS domains)
autoConvertOptions: {
// Default embed options for auto-converted videos
showTitle: true,
linkTitle: true,
showRelated: true,
showUserAvatar: true,
},
},
});
registerOption(ltiName, {
processor: 'object',
"default": {
// LTI configuration for MediaCMS iframe library
toolId: 0, // LTI external tool ID
courseId: 0, // Current course ID
contentItemUrl: '', // URL to /mod/lti/contentitem.php for Deep Linking
},
});
};
/**
* Get the permissions configuration for the Tiny Media plugin.
*
* @param {TinyMCE} editor
* @returns {object}
*/
export const getPermissions = (editor) => editor.options.get(permissionsName);
/**
* Get the permissions configuration for the Tiny Media plugin.
*
* @param {TinyMCE} editor
* @returns {object}
*/
export const getImagePermissions = (editor) => getPermissions(editor).image;
/**
* Get the permissions configuration for the Tiny Media plugin.
*
* @param {TinyMCE} editor
* @returns {object}
*/
export const getEmbedPermissions = (editor) => getPermissions(editor).embed;
/**
* Get the data configuration for the Media Manager.
*
* @param {TinyMCE} editor
* @returns {object}
*/
export const getData = (editor) => editor.options.get(dataName);
/**
* Get the LTI configuration for the MediaCMS iframe library.
*
* @param {TinyMCE} editor
* @returns {object}
*/
export const getLti = (editor) => editor.options.get(ltiName);

View File

@@ -0,0 +1,184 @@
// 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 tiny_mediacms/plugin
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import {getTinyMCE} from 'editor_tiny/loader';
import {getPluginMetadata} from 'editor_tiny/utils';
import {component, pluginName} from './common';
import * as Commands from './commands';
import * as Configuration from './configuration';
import * as Options from './options';
import {setupAutoConvert} from './autoconvert';
/**
* Check if a URL is a MediaCMS URL (embed or view).
*
* @param {string} url - The URL to check
* @returns {boolean} True if it's a MediaCMS URL
*/
const isMediaCMSUrl = (url) => {
if (!url) {
return false;
}
try {
const urlObj = new URL(url);
// Match both /embed and /view paths with ?m= parameter
return (urlObj.pathname === '/embed' || urlObj.pathname === '/view') && urlObj.searchParams.has('m');
} catch (e) {
return false;
}
};
/**
* Convert a MediaCMS URL (embed or view) to an iframe HTML string.
* If it's a view URL, it will be converted to embed URL.
*
* @param {string} url - The MediaCMS URL
* @returns {string} The iframe HTML
*/
const mediaCMSUrlToIframe = (url) => {
// Convert view URL to embed URL if needed
let embedUrl = url;
try {
const urlObj = new URL(url);
if (urlObj.pathname === '/view') {
urlObj.pathname = '/embed';
embedUrl = urlObj.toString();
}
} catch (e) {
// Keep original URL if parsing fails
}
return `<iframe src="${embedUrl}" ` +
`style="width: 100%; aspect-ratio: 16 / 9; display: block; border: 0;" ` +
`allowfullscreen="allowfullscreen"></iframe>`;
};
/**
* Regular expression to match standalone MediaCMS URLs in content.
* Matches URLs that are on their own line or surrounded by whitespace/tags.
* The URL must contain /embed?m= or /view?m= pattern.
*/
const MEDIACMS_URL_PATTERN = /(^|>|\s)(https?:\/\/[^\s<>"]+\/(?:embed|view)\?m=[^\s<>"]+)(<|\s|$)/g;
// eslint-disable-next-line no-async-promise-executor
export default new Promise(async(resolve) => {
const [
tinyMCE,
setupCommands,
pluginMetadata,
] = await Promise.all([
getTinyMCE(),
Commands.getSetup(),
getPluginMetadata(component, pluginName),
]);
tinyMCE.PluginManager.add(`${component}/plugin`, (editor) => {
// Register options.
Options.register(editor);
// Setup the Commands (buttons, menu items, and so on).
setupCommands(editor);
// Setup auto-conversion of pasted MediaCMS URLs.
setupAutoConvert(editor);
// Convert MediaCMS URLs to iframes when content is loaded into the editor.
// This handles content from the database that was saved as just URLs.
editor.on('BeforeSetContent', (e) => {
if (e.content && typeof e.content === 'string') {
// Replace standalone MediaCMS URLs with iframes
e.content = e.content.replace(MEDIACMS_URL_PATTERN, (match, before, url, after) => {
// Verify it's a valid MediaCMS URL
if (isMediaCMSUrl(url)) {
return before + mediaCMSUrlToIframe(url) + after;
}
return match;
});
}
});
// Convert MediaCMS iframes back to just embed URLs when saving.
// This stores only the URL in the database, not the full iframe HTML.
editor.on('GetContent', (e) => {
if (e.format === 'html') {
// Create a temporary container to manipulate the HTML
const tempDiv = document.createElement('div');
tempDiv.innerHTML = e.content;
// Remove edit buttons
tempDiv.querySelectorAll('.tiny-mediacms-edit-btn').forEach(btn => btn.remove());
// Process all iframes - convert MediaCMS iframes to just URLs
tempDiv.querySelectorAll('iframe').forEach(iframe => {
const src = iframe.getAttribute('src');
if (isMediaCMSUrl(src)) {
// Check if iframe is inside a wrapper
const wrapper = iframe.closest('.tiny-mediacms-iframe-wrapper') ||
iframe.closest('.tiny-iframe-responsive');
// Create a text node with just the URL
const urlText = document.createTextNode(src);
// Wrap in a paragraph for proper formatting
const p = document.createElement('p');
p.appendChild(urlText);
if (wrapper) {
// Replace the entire wrapper with the URL
wrapper.parentNode.insertBefore(p, wrapper);
wrapper.remove();
} else {
// Replace just the iframe with the URL
iframe.parentNode.insertBefore(p, iframe);
iframe.remove();
}
}
});
// Clean up any remaining wrappers that might not have had MediaCMS iframes
tempDiv.querySelectorAll('.tiny-mediacms-iframe-wrapper').forEach(wrapper => {
const iframe = wrapper.querySelector('iframe');
if (iframe) {
wrapper.parentNode.insertBefore(iframe, wrapper);
}
wrapper.remove();
});
tempDiv.querySelectorAll('.tiny-iframe-responsive').forEach(wrapper => {
const iframe = wrapper.querySelector('iframe');
if (iframe) {
wrapper.parentNode.insertBefore(iframe, wrapper);
}
wrapper.remove();
});
e.content = tempDiv.innerHTML;
}
});
return pluginMetadata;
});
// Resolve the Media Plugin and include configuration.
resolve([`${component}/plugin`, Configuration]);
});

View File

@@ -0,0 +1,162 @@
// 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 helper function to build queryable data selectors.
*
* @module tiny_mediacms/selectors
* @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
export default {
IMAGE: {
actions: {
submit: '.tiny_imagecms_urlentrysubmit',
imageBrowser: '.openimagecmsbrowser',
addUrl: '.tiny_imagecms_addurl',
deleteImage: '.tiny_imagecms_deleteicon',
},
elements: {
form: 'form.tiny_imagecms_form',
alignSettings: '.tiny_imagecms_button',
alt: '.tiny_imagecms_altentry',
altWarning: '.tiny_imagecms_altwarning',
height: '.tiny_imagecms_heightentry',
width: '.tiny_imagecms_widthentry',
url: '.tiny_imagecms_urlentry',
urlWarning: '.tiny_imagecms_urlwarning',
size: '.tiny_imagecms_size',
presentation: '.tiny_imagecms_presentation',
constrain: '.tiny_imagecms_constrain',
customStyle: '.tiny_imagecms_customstyle',
preview: '.tiny_imagecms_preview',
previewBox: '.tiny_imagecms_preview_box',
loaderIcon: '.tiny_imagecms_loader',
loaderIconContainer: '.tiny_imagecms_loader_container',
insertImage: '.tiny_imagecms_insert_image',
modalFooter: '.modal-footer',
dropzoneContainer: '.tiny_imagecms_dropzone_container',
fileInput: '#tiny_imagecms_fileinput',
fileNameLabel: '.tiny_imagecms_filename',
sizeOriginal: '.tiny_imagecms_sizeoriginal',
sizeCustom: '.tiny_imagecms_sizecustom',
properties: '.tiny_imagecms_properties',
},
styles: {
responsive: 'img-fluid',
},
},
EMBED: {
actions: {
submit: '.tiny_mediacms_submit',
mediaBrowser: '.openmediacmsbrowser',
},
elements: {
form: 'form.tiny_mediacms_form',
source: '.tiny_mediacms_source',
track: '.tiny_mediacms_track',
mediaSource: '.tiny_mediacms_media_source',
linkSource: '.tiny_mediacms_link_source',
linkSize: '.tiny_mediacms_link_size',
posterSource: '.tiny_mediacms_poster_source',
posterSize: '.tiny_mediacms_poster_size',
displayOptions: '.tiny_mediacms_display_options',
name: '.tiny_mediacms_name_entry',
title: '.tiny_mediacms_title_entry',
url: '.tiny_mediacms_url_entry',
width: '.tiny_mediacms_width_entry',
height: '.tiny_mediacms_height_entry',
trackSource: '.tiny_mediacms_track_source',
trackKind: '.tiny_mediacms_track_kind_entry',
trackLabel: '.tiny_mediacms_track_label_entry',
trackLang: '.tiny_mediacms_track_lang_entry',
trackDefault: '.tiny_mediacms_track_default',
mediaControl: '.tiny_mediacms_controls',
mediaAutoplay: '.tiny_mediacms_autoplay',
mediaMute: '.tiny_mediacms_mute',
mediaLoop: '.tiny_mediacms_loop',
advancedSettings: '.tiny_mediacms_advancedsettings',
linkTab: 'li[data-medium-type="link"]',
videoTab: 'li[data-medium-type="video"]',
audioTab: 'li[data-medium-type="audio"]',
linkPane: '.tab-pane[data-medium-type="link"]',
videoPane: '.tab-pane[data-medium-type="video"]',
audioPane: '.tab-pane[data-medium-type="audio"]',
trackSubtitlesTab: 'li[data-track-kind="subtitles"]',
trackCaptionsTab: 'li[data-track-kind="captions"]',
trackDescriptionsTab: 'li[data-track-kind="descriptions"]',
trackChaptersTab: 'li[data-track-kind="chapters"]',
trackMetadataTab: 'li[data-track-kind="metadata"]',
trackSubtitlesPane: '.tab-pane[data-track-kind="subtitles"]',
trackCaptionsPane: '.tab-pane[data-track-kind="captions"]',
trackDescriptionsPane: '.tab-pane[data-track-kind="descriptions"]',
trackChaptersPane: '.tab-pane[data-track-kind="chapters"]',
trackMetadataPane: '.tab-pane[data-track-kind="metadata"]',
},
mediaTypes: {
link: 'LINK',
video: 'VIDEO',
audio: 'AUDIO',
},
trackKinds: {
subtitles: 'SUBTITLES',
captions: 'CAPTIONS',
descriptions: 'DESCRIPTIONS',
chapters: 'CHAPTERS',
metadata: 'METADATA',
},
},
IFRAME: {
actions: {
remove: '[data-action="remove"]',
},
elements: {
form: 'form.tiny_iframecms_form',
url: '.tiny_iframecms_url',
urlWarning: '.tiny_iframecms_url_warning',
showTitle: '.tiny_iframecms_showtitle',
linkTitle: '.tiny_iframecms_linktitle',
showRelated: '.tiny_iframecms_showrelated',
showUserAvatar: '.tiny_iframecms_showuseravatar',
responsive: '.tiny_iframecms_responsive',
startAt: '.tiny_iframecms_startat',
startAtEnabled: '.tiny_iframecms_startat_enabled',
aspectRatio: '.tiny_iframecms_aspectratio',
width: '.tiny_iframecms_width',
height: '.tiny_iframecms_height',
preview: '.tiny_iframecms_preview',
previewContainer: '.tiny_iframecms_preview_container',
// Tab elements
tabs: '.tiny_iframecms_tabs',
tabUrlBtn: '.tiny_iframecms_tab_url_btn',
tabIframeLibraryBtn: '.tiny_iframecms_tab_iframe_library_btn',
paneUrl: '.tiny_iframecms_pane_url',
paneIframeLibrary: '.tiny_iframecms_pane_iframe_library',
// Iframe library elements
iframeLibraryContainer: '.tiny_iframecms_iframe_library_container',
iframeLibraryPlaceholder:
'.tiny_iframecms_iframe_library_placeholder',
iframeLibraryLoading: '.tiny_iframecms_iframe_library_loading',
iframeLibraryFrame: '.tiny_iframecms_iframe_library_frame',
},
aspectRatios: {
'16:9': { width: 560, height: 315 },
'4:3': { width: 560, height: 420 },
'1:1': { width: 400, height: 400 },
custom: null,
},
},
};

View File

@@ -0,0 +1,95 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Tiny Media Manager usedfiles.
*
* @module tiny_mediacms/usedfiles
* @copyright 2022, Stevani Andolo <stevani@hotmail.com.au>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import * as Templates from 'core/templates';
import Config from 'core/config';
class UsedFileManager {
constructor(files, userContext, itemId, elementId) {
this.files = files;
this.userContext = userContext;
this.itemId = itemId;
this.elementId = elementId;
}
getElementId() {
return this.elementId;
}
getUsedFiles() {
const editor = window.parent.tinymce.EditorManager.get(this.getElementId());
if (!editor) {
window.console.error(`Editor not found for ${this.getElementId()}`);
return [];
}
const content = editor.getContent();
const baseUrl = `${Config.wwwroot}/draftfile.php/${this.userContext}/user/draft/${this.itemId}/`;
const pattern = new RegExp("[\"']" + baseUrl.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') + "(?<filename>.+?)[\\?\"']", 'gm');
const usedFiles = [...content.matchAll(pattern)].map((match) => decodeURIComponent(match.groups.filename));
return usedFiles;
}
// Return an array of unused files.
findUnusedFiles(usedFiles) {
return Object.entries(this.files)
.filter(([filename]) => !usedFiles.includes(filename))
.map(([filename]) => filename);
}
// Return an array of missing files.
findMissingFiles(usedFiles) {
return usedFiles.filter((filename) => !this.files.hasOwnProperty(filename));
}
updateFiles() {
const form = document.querySelector('form');
const usedFiles = this.getUsedFiles();
const unusedFiles = this.findUnusedFiles(usedFiles);
const missingFiles = this.findMissingFiles(usedFiles);
form.querySelectorAll('input[type=checkbox][name^="deletefile"]').forEach((checkbox) => {
if (!unusedFiles.includes(checkbox.dataset.filename)) {
checkbox.closest('.fitem').remove();
}
});
form.classList.toggle('has-missing-files', !!missingFiles.length);
form.classList.toggle('has-unused-files', !!unusedFiles.length);
return Templates.renderForPromise('tiny_mediacms/missingfiles', {
missingFiles,
}).then(({html, js}) => {
Templates.replaceNodeContents(form.querySelector('.missing-files'), html, js);
return;
});
}
}
export const init = (files, usercontext, itemid, elementid) => {
const manager = new UsedFileManager(files, usercontext, itemid, elementid);
manager.updateFiles();
return manager;
};