diff --git a/lib/classes/plugin_manager.php b/lib/classes/plugin_manager.php index 94d86de596410..7d3a77a36d8fa 100644 --- a/lib/classes/plugin_manager.php +++ b/lib/classes/plugin_manager.php @@ -2031,6 +2031,7 @@ public static function standard_plugins_list($type) { 'tiny' => [ 'autosave', 'h5p', + 'media', ], 'tinymce' => array( diff --git a/lib/editor/tiny/plugins/media/amd/build/commands.min.js b/lib/editor/tiny/plugins/media/amd/build/commands.min.js new file mode 100644 index 0000000000000..5ef6520340f21 --- /dev/null +++ b/lib/editor/tiny/plugins/media/amd/build/commands.min.js @@ -0,0 +1,3 @@ +define("tiny_media/commands",["exports","core/str","./common","./image"],(function(_exports,_str,_common,_image){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.getSetup=void 0;_exports.getSetup=async()=>{const[imageButtonText]=await Promise.all([(0,_str.get_string)("imagebuttontitle",_common.component)]);return editor=>{const mediaImage=new _image.MediaImage(editor);editor.ui.registry.addToggleButton(_common.imageButtonName,{icon:"image",text:imageButtonText,tooltip:imageButtonText,onAction:()=>{mediaImage.displayDialogue()},onSetup:api=>editor.selection.selectorChangedWithUnbind("img:not([data-mce-object]):not([data-mce-placeholder]),figure.image",api.setActive).unbind}),editor.ui.registry.addMenuItem(_common.imageButtonName,{icon:"image",text:imageButtonText,onAction:()=>{mediaImage.displayDialogue()}}),editor.ui.registry.addContextToolbar(_common.imageButtonName,{predicate:node=>"img"===node.nodeName.toLowerCase(),items:_common.imageButtonName,position:"node",scope:"node"})}}})); + +//# sourceMappingURL=commands.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/media/amd/build/commands.min.js.map b/lib/editor/tiny/plugins/media/amd/build/commands.min.js.map new file mode 100644 index 0000000000000..9bd320bcc0673 --- /dev/null +++ b/lib/editor/tiny/plugins/media/amd/build/commands.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"commands.min.js","sources":["../src/commands.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Tiny Media commands.\n *\n * @module tiny_media/commands\n * @copyright 2022 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {get_string as getString} from 'core/str';\nimport {\n component,\n imageButtonName,\n} from './common';\nimport {MediaImage} from './image';\n\nexport const getSetup = async() => {\n const [\n imageButtonText,\n ] = await Promise.all([\n getString('imagebuttontitle', component),\n ]);\n\n return (editor) => {\n const mediaImage = new MediaImage(editor);\n const icon = 'image';\n\n // Register the Menu Button as a toggle.\n // This means that when highlighted over an existing Media Image element it will show as toggled on.\n editor.ui.registry.addToggleButton(imageButtonName, {\n icon,\n text: imageButtonText,\n tooltip: imageButtonText,\n onAction: () => {mediaImage.displayDialogue();},\n onSetup: api => {\n return editor.selection.selectorChangedWithUnbind(\n 'img:not([data-mce-object]):not([data-mce-placeholder]),figure.image',\n api.setActive\n ).unbind;\n }\n });\n\n editor.ui.registry.addMenuItem(imageButtonName, {\n icon,\n text: imageButtonText,\n onAction: () => {mediaImage.displayDialogue();}\n });\n\n editor.ui.registry.addContextToolbar(imageButtonName, {\n predicate: node => {\n return node.nodeName.toLowerCase() === 'img';\n },\n items: imageButtonName,\n position: 'node',\n scope: 'node'\n });\n };\n};\n"],"names":["async","imageButtonText","Promise","all","component","editor","mediaImage","MediaImage","ui","registry","addToggleButton","imageButtonName","icon","text","tooltip","onAction","displayDialogue","onSetup","api","selection","selectorChangedWithUnbind","setActive","unbind","addMenuItem","addContextToolbar","predicate","node","nodeName","toLowerCase","items","position","scope"],"mappings":"oNA8BwBA,gBAEhBC,uBACMC,QAAQC,IAAI,EAClB,mBAAU,mBAAoBC,4BAG1BC,eACEC,WAAa,IAAIC,kBAAWF,QAKlCA,OAAOG,GAAGC,SAASC,gBAAgBC,wBAAiB,CAChDC,KALS,QAMTC,KAAMZ,gBACNa,QAASb,gBACTc,SAAU,KAAOT,WAAWU,mBAC5BC,QAASC,KACEb,OAAOc,UAAUC,0BACpB,sEACAF,IAAIG,WACNC,SAIVjB,OAAOG,GAAGC,SAASc,YAAYZ,wBAAiB,CAC5CC,KAlBS,QAmBTC,KAAMZ,gBACNc,SAAU,KAAOT,WAAWU,qBAGhCX,OAAOG,GAAGC,SAASe,kBAAkBb,wBAAiB,CAClDc,UAAWC,MACgC,QAAhCA,KAAKC,SAASC,cAEzBC,MAAOlB,wBACPmB,SAAU,OACVC,MAAO"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/media/amd/build/common.min.js b/lib/editor/tiny/plugins/media/amd/build/common.min.js new file mode 100644 index 0000000000000..a8ae65945d069 --- /dev/null +++ b/lib/editor/tiny/plugins/media/amd/build/common.min.js @@ -0,0 +1,3 @@ +define("tiny_media/common",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default={pluginName:"tiny_media/plugin",component:"tiny_media",imageButtonName:"tiny_media"},_exports.default})); + +//# sourceMappingURL=common.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/media/amd/build/common.min.js.map b/lib/editor/tiny/plugins/media/amd/build/common.min.js.map new file mode 100644 index 0000000000000..270f699a2c531 --- /dev/null +++ b/lib/editor/tiny/plugins/media/amd/build/common.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"common.min.js","sources":["../src/common.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Tiny Media common values.\n *\n * @module tiny_media/common\n * @copyright 2022 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nexport default {\n pluginName: 'tiny_media/plugin',\n component: 'tiny_media',\n imageButtonName: 'tiny_media',\n};\n"],"names":["pluginName","component","imageButtonName"],"mappings":"mKAuBe,CACXA,WAAY,oBACZC,UAAW,aACXC,gBAAiB"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/media/amd/build/configuration.min.js b/lib/editor/tiny/plugins/media/amd/build/configuration.min.js new file mode 100644 index 0000000000000..05f65070daaae --- /dev/null +++ b/lib/editor/tiny/plugins/media/amd/build/configuration.min.js @@ -0,0 +1,10 @@ +define("tiny_media/configuration",["exports","./common","./upload-handler","editor_tiny/utils"],(function(_exports,_common,_uploadHandler,_utils){var obj; +/** + * Tiny Media configuration. + * + * @module tiny_media/configuration + * @copyright 2022 Huong Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.configure=void 0,_uploadHandler=(obj=_uploadHandler)&&obj.__esModule?obj:{default:obj};_exports.configure=instanceConfig=>({contextmenu:(0,_utils.addContextmenuItem)(instanceConfig.contextmenu,_common.imageButtonName),toolbar:(0,_utils.addToolbarButton)(instanceConfig.toolbar,"content",[_common.imageButtonName]),menu:(0,_utils.addMenubarItem)(instanceConfig.menu,"insert",_common.imageButtonName),quickbars_insert_toolbar:(0,_utils.addContextmenuItem)(instanceConfig.quickbars_insert_toolbar,_common.imageButtonName),images_upload_handler:(blobInfo,progress)=>(0,_uploadHandler.default)(window.tinymce.activeEditor,blobInfo,progress)})})); + +//# sourceMappingURL=configuration.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/media/amd/build/configuration.min.js.map b/lib/editor/tiny/plugins/media/amd/build/configuration.min.js.map new file mode 100644 index 0000000000000..66d18bbc4855e --- /dev/null +++ b/lib/editor/tiny/plugins/media/amd/build/configuration.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"configuration.min.js","sources":["../src/configuration.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Tiny Media configuration.\n *\n * @module tiny_media/configuration\n * @copyright 2022 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {\n imageButtonName,\n} from './common';\nimport uploadHandler from \"./upload-handler\";\nimport {\n addContextmenuItem,\n addMenubarItem,\n addToolbarButton,\n} from 'editor_tiny/utils';\n\nexport const configure = (instanceConfig) => {\n // Update the instance configuration to add the Media menu option to the menus and toolbars and upload_handler.\n return {\n contextmenu: addContextmenuItem(instanceConfig.contextmenu, imageButtonName),\n toolbar: addToolbarButton(instanceConfig.toolbar, 'content', [imageButtonName]),\n menu: addMenubarItem(instanceConfig.menu, 'insert', imageButtonName),\n quickbars_insert_toolbar: addContextmenuItem(instanceConfig.quickbars_insert_toolbar, imageButtonName),\n images_upload_handler: (blobInfo, progress) => uploadHandler(window.tinymce.activeEditor, blobInfo, progress)\n };\n};\n"],"names":["instanceConfig","contextmenu","imageButtonName","toolbar","menu","quickbars_insert_toolbar","images_upload_handler","blobInfo","progress","window","tinymce","activeEditor"],"mappings":";;;;;;;gLAiC0BA,iBAEf,CACHC,aAAa,6BAAmBD,eAAeC,YAAaC,yBAC5DC,SAAS,2BAAiBH,eAAeG,QAAS,UAAW,CAACD,0BAC9DE,MAAM,yBAAeJ,eAAeI,KAAM,SAAUF,yBACpDG,0BAA0B,6BAAmBL,eAAeK,yBAA0BH,yBACtFI,sBAAuB,CAACC,SAAUC,YAAa,0BAAcC,OAAOC,QAAQC,aAAcJ,SAAUC"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/media/amd/build/image.min.js b/lib/editor/tiny/plugins/media/amd/build/image.min.js new file mode 100644 index 0000000000000..a0ce4e8572296 --- /dev/null +++ b/lib/editor/tiny/plugins/media/amd/build/image.min.js @@ -0,0 +1,3 @@ +define("tiny_media/image",["exports","core/templates","core/str","core/modal_factory","core/modal_events","editor_tiny/utils"],(function(_exports,_templates,_str,Modal,ModalEvents,_utils){var obj;function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj}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.MediaImage=void 0,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj},Modal=_interopRequireWildcard(Modal),ModalEvents=_interopRequireWildcard(ModalEvents);_exports.MediaImage=class{constructor(editor){_defineProperty(this,"CSS",{FORM:"form.tiny_image_form",RESPONSIVE:"img-fluid",INPUTALIGNMENT:"tiny_image_alignment",INPUTALT:"tiny_image_altentry",INPUTHEIGHT:"tiny_image_heightentry",INPUTSUBMIT:"tiny_image_urlentrysubmit",INPUTURL:"tiny_image_urlentry",INPUTSIZE:"tiny_image_size",INPUTWIDTH:"tiny_image_widthentry",IMAGEALTWARNING:"tiny_image_altwarning",IMAGEURLWARNING:"tiny_image_urlwarning",IMAGEBROWSER:"openimagebrowser",IMAGEPRESENTATION:"tiny_image_presentation",INPUTCONSTRAIN:"tiny_image_constrain",INPUTCUSTOMSTYLE:"tiny_image_customstyle",IMAGEPREVIEW:"tiny_image_preview",IMAGEPREVIEWBOX:"tiny_image_preview_box",ALIGNSETTINGS:"tiny_image_button"}),_defineProperty(this,"FORMNAMES",{URL:"urlentry",ALT:"altentry"}),_defineProperty(this,"REGEX",{ISPERCENT:/\d+%/}),_defineProperty(this,"DEFAULTS",{WIDTH:160,HEIGHT:160}),_defineProperty(this,"ALIGNMENTS",[{name:"verticalAlign",str:"alignment_top",value:"text-top",margin:"0 0.5em"},{name:"verticalAlign",str:"alignment_middle",value:"middle",margin:"0 0.5em"},{name:"verticalAlign",str:"alignment_bottom",value:"text-bottom",margin:"0 0.5em",isDefault:!0},{name:"float",str:"alignment_left",value:"left",margin:"0 0.5em 0 0"},{name:"float",str:"alignment_right",value:"right",margin:"0 0 0 0.5em"}]),_defineProperty(this,"form",null),_defineProperty(this,"rawImageDimensions",null),_defineProperty(this,"canShowFilePicker",!0),_defineProperty(this,"editor",null),_defineProperty(this,"currentModal",null),_defineProperty(this,"selectedImage",null),this.editor=editor}displayDialogue(){this.rawImageDimensions=null,Modal.create({type:Modal.types.DEFAULT,title:(0,_str.get_string)("imageproperties","tiny_media"),body:_templates.default.render("tiny_media/insert_image",{elementid:this.editor.getElement().id,CSS:this.CSS,FORMNAMES:this.FORMNAMES,showfilepicker:this.canShowFilePicker})}).then((modal=>(this.currentModal=modal,modal.getRoot().on(ModalEvents.bodyRendered,(()=>{this.form=document.querySelector(this.CSS.FORM),this.applyImageProperties(),this.registerEventListeners()})),modal.getRoot().on(ModalEvents.hidden,(()=>{modal.destroy()})),modal.show(),modal)))}filePickerCallback(params,self){if(""!==params.url){self.form.querySelector("."+self.CSS.INPUTURL).value=params.url,self.form.querySelector("."+self.CSS.INPUTWIDTH).value="",self.form.querySelector("."+self.CSS.INPUTHEIGHT).value="",self.loadPreviewImage(params.url)}}loadPreviewImage(url){const image=new Image;image.onerror=()=>{this.form.querySelector("."+CSS.IMAGEPREVIEW).style.display="none"},image.onload=()=>{let input,currentWidth,currentHeight,widthRatio,heightRatio;this.rawImageDimensions={width:image.width||this.DEFAULTS.WIDTH,height:image.height||this.DEFAULTS.HEIGHT},input=this.form.querySelector("."+this.CSS.INPUTWIDTH),currentWidth=input.value,""===currentWidth&&(input.value=this.rawImageDimensions.width,currentWidth=""+this.rawImageDimensions.width),input=this.form.querySelector("."+this.CSS.INPUTHEIGHT),currentHeight=input.value,""===currentHeight&&(input.value=this.rawImageDimensions.height,currentHeight=""+this.rawImageDimensions.height),input=this.form.querySelector("."+this.CSS.IMAGEPREVIEW),input.setAttribute("src",image.src),input.style.display="inline",input=this.form.querySelector("."+this.CSS.INPUTCONSTRAIN),currentWidth.match(this.REGEX.ISPERCENT)&¤tHeight.match(this.REGEX.ISPERCENT)?input.checked=currentWidth===currentHeight:0===image.width||0===image.height?input.disabled="disabled":(widthRatio=Math.round(1e3*parseInt(currentWidth,10)/image.width),heightRatio=Math.round(1e3*parseInt(currentHeight,10)/image.height),input.checked=widthRatio===heightRatio)},image.src=url}urlChanged(){const input=this.form.querySelector("."+this.CSS.INPUTURL);""!==input.value&&this.loadPreviewImage(input.value)}hasErrorUrlField(){const urlError=""===this.form.querySelector("."+this.CSS.INPUTURL).value;return this.toggleVisibility("."+this.CSS.IMAGEURLWARNING,urlError),this.toggleAriaInvalid(["."+this.CSS.INPUTURL],urlError),urlError}hasErrorAltField(){const alt=this.form.querySelector("."+this.CSS.INPUTALT).value,presentation=this.form.querySelector("."+this.CSS.IMAGEPRESENTATION).checked,imageAltError=""===alt&&!presentation;return this.toggleVisibility("."+this.CSS.IMAGEALTWARNING,imageAltError),this.toggleAriaInvalid(["."+this.CSS.INPUTALT,"."+this.CSS.IMAGEPRESENTATION],imageAltError),imageAltError}toggleVisibility(selector,predicate){this.form.querySelectorAll(selector).forEach((element=>{element.style.display=predicate?"block":"none"}))}toggleAriaInvalid(selectors,predicate){selectors.forEach((selector=>{this.form.querySelectorAll(selector).forEach((element=>{element.setAttribute("aria-invalid",predicate)}))}))}getAlignmentClass(alignment){return this.CSS.ALIGNSETTINGS+"_"+alignment}updateWarning(){const urlError=this.hasErrorUrlField(),imageAltError=this.hasErrorAltField();return urlError||imageAltError}setImage(e){const url=this.form.querySelector("."+this.CSS.INPUTURL).value,alt=this.form.querySelector("."+this.CSS.INPUTALT).value,width=this.form.querySelector("."+this.CSS.INPUTWIDTH).value,height=this.form.querySelector("."+this.CSS.INPUTHEIGHT).value,alignment=this.getAlignmentClass(this.form.querySelector("."+this.CSS.INPUTALIGNMENT).value),presentation=this.form.querySelector("."+this.CSS.IMAGEPRESENTATION).checked,constrain=this.form.querySelector("."+this.CSS.INPUTCONSTRAIN).value,customStyle=this.form.querySelector("."+this.CSS.INPUTCUSTOMSTYLE).value;let imageHtml,classList=[];if(e.preventDefault(),!this.updateWarning()&&""!==url){if(constrain&&classList.push(this.CSS.RESPONSIVE),classList.push(alignment),!width.match(this.REGEX.ISPERCENT)&&isNaN(parseInt(width,10)))return void this.form.querySelector("."+this.CSS.INPUTWIDTH).focus();if(!height.match(this.REGEX.ISPERCENT)&&isNaN(parseInt(height,10)))return void this.form.querySelector("."+this.CSS.INPUTHEIGHT).focus();_templates.default.render("tiny_media/image",{url:url,alt:alt,width:width,height:height,presentation:presentation,customstyle:customStyle,classlist:classList.join(" ")}).then((html=>{imageHtml=html,this.editor.insertContent(imageHtml),this.currentModal.destroy()}))}}handleKeyupCharacterCount(){const alt=this.form.querySelector("."+this.CSS.INPUTALT).value;this.form.querySelector("#currentcount").innerHTML=alt.length}autoAdjustSize(e,forceHeight){forceHeight=forceHeight||!1;let rawPercentage,rawSize,keyField=this.form.querySelector("."+this.CSS.INPUTWIDTH),keyFieldType="width",subField=this.form.querySelector("."+this.CSS.INPUTHEIGHT),subFieldType="height",constrainField=this.form.querySelector("."+this.CSS.INPUTCONSTRAIN),keyFieldValue=keyField.value,subFieldValue=subField.value,imagePreview=this.form.querySelector("."+this.CSS.IMAGEPREVIEW);if(this.rawImageDimensions)if(""===keyFieldValue&&(keyFieldValue=this.rawImageDimensions[keyFieldType],keyField.value=keyFieldValue,keyFieldValue=keyField.value),imagePreview.style.width=null,imagePreview.style.height=null,constrainField.checked){if(forceHeight){let temporaryValue;temporaryValue=keyField,keyField=subField,subField=temporaryValue,temporaryValue=keyFieldType,keyFieldType=subFieldType,subFieldType=temporaryValue,temporaryValue=keyFieldValue,keyFieldValue=subFieldValue,subFieldValue=temporaryValue}keyFieldValue.match(this.REGEX.ISPERCENT)?(subFieldValue=keyFieldValue,rawPercentage=parseInt(keyFieldValue,10),rawSize=this.rawImageDimensions.width/100*rawPercentage,imagePreview.style.width=rawSize,rawSize=this.rawImageDimensions.height/100*rawPercentage,imagePreview.style.height=rawSize):(subFieldValue=Math.round(keyFieldValue/this.rawImageDimensions[keyFieldType]*this.rawImageDimensions[subFieldType]),forceHeight?(imagePreview.style.width=subFieldValue,imagePreview.style.height=keyFieldValue):(imagePreview.style.width=keyFieldValue,imagePreview.style.height=subFieldValue)),subField.value=subFieldValue}else keyFieldValue.match(this.REGEX.ISPERCENT)?(rawPercentage=parseInt(keyFieldValue,10),rawSize=this.rawImageDimensions.width/100*rawPercentage,imagePreview.style.width=rawSize+"px"):imagePreview.style.width=keyFieldValue+"px",subFieldValue.match(this.REGEX.ISPERCENT)?(rawPercentage=parseInt(subFieldValue,10),rawSize=this.rawImageDimensions.height/100*rawPercentage,imagePreview.style.height=rawSize+"px"):imagePreview.style.height=subFieldValue+"px"}applyImageProperties(){const properties=this.getSelectedImageProperties(),img=this.form.querySelector("."+this.CSS.IMAGEPREVIEW);if(!1===properties)return img.style.display="none",void this.ALIGNMENTS.some((alignment=>!!alignment.isDefault&&(this.form.querySelector("."+this.CSS.INPUTALIGNMENT).value=alignment.value,!0)));properties.align&&(this.form.querySelector("."+this.CSS.INPUTALIGNMENT).value=properties.align),properties.customstyle&&(this.form.querySelector("."+this.CSS.INPUTCUSTOMSTYLE).value=properties.customstyle),properties.width&&(this.form.querySelector("."+this.CSS.INPUTWIDTH).value=properties.width),properties.height&&(this.form.querySelector("."+this.CSS.INPUTHEIGHT).value=properties.height),properties.alt&&(this.form.querySelector("."+this.CSS.INPUTALT).value=properties.alt),properties.src&&(this.form.querySelector("."+this.CSS.INPUTURL).value=properties.src,this.loadPreviewImage(properties.src)),properties.presentation&&(this.form.querySelector("."+this.CSS.IMAGEPRESENTATION).checked="checked"),this.autoAdjustSize()}getSelectedImageProperties(){let width,height,style,properties={src:null,alt:null,width:null,height:null,align:"",presentation:!1},image=this.getSelectedImage();return image?(image=this.removeLegacyAlignment(image),this.selectedImage=image,style=image.style,properties.customstyle=style,width=image.width,String(width).match(this.REGEX.ISPERCENT)||(width=parseInt(width,10)),height=image.height,String(height).match(this.REGEX.ISPERCENT)||(height=parseInt(height,10)),0!==width&&(properties.width=width),0!==height&&(properties.height=height),this.getAlignmentProperties(image,properties),properties.src=image.getAttribute("src"),properties.alt=image.getAttribute("alt")||"",properties.presentation="presentation"===image.getAttribute("role"),properties):(this.selectedImage=null,!1)}removeLegacyAlignment(imageNode){return imageNode.style.margin?(this.ALIGNMENTS.some((alignment=>{if(imageNode.style[alignment.name]!==alignment.value)return!1;const normalisedNode=document.createElement("div");return normalisedNode.style.margin=alignment.margin,imageNode.style.margin===normalisedNode.style.margin&&(imageNode.classList.add(this.getAlignmentClass(alignment.value)),imageNode.style[alignment.name]=null,imageNode.style.margin=null,!0)})),imageNode):imageNode}getAlignmentProperties(image,properties){let defaultAlignment,complete=!1;complete=this.ALIGNMENTS.some((alignment=>{const classname=this.getAlignmentClass(alignment.value);return image.classList.contains(classname)?(properties.align=alignment.value,!0):(alignment.isDefault&&(defaultAlignment=alignment.value),!1)})),!complete&&defaultAlignment&&(properties.align=defaultAlignment)}getSelectedImage(){const imgElm=this.editor.selection.getNode(),figureElm=this.editor.dom.getParent(imgElm,"figure.image");return figureElm?this.editor.dom.select("img",figureElm)[0]:imgElm&&("IMG"!==imgElm.nodeName||this.isPlaceholderImage(imgElm))?null:imgElm}isPlaceholderImage(imgElm){return"IMG"===imgElm.nodeName&&(imgElm.hasAttribute("data-mce-object")||imgElm.hasAttribute("data-mce-placeholder"))}registerEventListeners(){const self=this;this.form.querySelector("."+this.CSS.INPUTURL).addEventListener("blur",(()=>{this.urlChanged()})),this.form.querySelector("."+this.CSS.INPUTURL).addEventListener("change",(()=>{this.hasErrorUrlField()})),this.form.querySelector("."+this.CSS.IMAGEPRESENTATION).addEventListener("change",(()=>{this.hasErrorAltField()})),this.form.querySelector("."+this.CSS.INPUTALT).addEventListener("blur",(()=>{this.hasErrorAltField()})),this.form.querySelector("."+this.CSS.INPUTWIDTH).addEventListener("blur",(e=>{this.autoAdjustSize(e)})),this.form.querySelector("."+this.CSS.INPUTHEIGHT).addEventListener("blur",(e=>{this.autoAdjustSize(e,!0)})),this.form.querySelector("."+this.CSS.INPUTSUBMIT).addEventListener("click",(e=>{this.setImage(e)})),this.canShowFilePicker&&this.form.querySelector("."+this.CSS.IMAGEBROWSER).addEventListener("click",(e=>{e.preventDefault(),(0,_utils.displayFilepicker)(this.editor,"image").then((params=>{this.filePickerCallback(params,self)})).catch()})),this.form.querySelector("."+this.CSS.INPUTALT).addEventListener("keyup",(()=>{this.handleKeyupCharacterCount()}))}}})); + +//# sourceMappingURL=image.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/media/amd/build/image.min.js.map b/lib/editor/tiny/plugins/media/amd/build/image.min.js.map new file mode 100644 index 0000000000000..0790684eb8d32 --- /dev/null +++ b/lib/editor/tiny/plugins/media/amd/build/image.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"image.min.js","sources":["../src/image.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Tiny Media plugin Image class for Moodle.\n *\n * @module tiny_media/image\n * @copyright 2022 Huong Nguyen \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Templates from 'core/templates';\nimport {get_string as getString} from 'core/str';\nimport * as Modal from 'core/modal_factory';\nimport * as ModalEvents from 'core/modal_events';\nimport {displayFilepicker} from 'editor_tiny/utils';\n\nexport const MediaImage = class {\n\n CSS = {\n FORM: 'form.tiny_image_form',\n RESPONSIVE: 'img-fluid',\n INPUTALIGNMENT: 'tiny_image_alignment',\n INPUTALT: 'tiny_image_altentry',\n INPUTHEIGHT: 'tiny_image_heightentry',\n INPUTSUBMIT: 'tiny_image_urlentrysubmit',\n INPUTURL: 'tiny_image_urlentry',\n INPUTSIZE: 'tiny_image_size',\n INPUTWIDTH: 'tiny_image_widthentry',\n IMAGEALTWARNING: 'tiny_image_altwarning',\n IMAGEURLWARNING: 'tiny_image_urlwarning',\n IMAGEBROWSER: 'openimagebrowser',\n IMAGEPRESENTATION: 'tiny_image_presentation',\n INPUTCONSTRAIN: 'tiny_image_constrain',\n INPUTCUSTOMSTYLE: 'tiny_image_customstyle',\n IMAGEPREVIEW: 'tiny_image_preview',\n IMAGEPREVIEWBOX: 'tiny_image_preview_box',\n ALIGNSETTINGS: 'tiny_image_button'\n };\n\n FORMNAMES = {\n URL: 'urlentry',\n ALT: 'altentry'\n };\n\n REGEX = {\n ISPERCENT: /\\d+%/\n };\n\n DEFAULTS = {\n WIDTH: 160,\n HEIGHT: 160,\n };\n\n ALIGNMENTS = [\n // Vertical alignment.\n {\n name: 'verticalAlign',\n str: 'alignment_top',\n value: 'text-top',\n margin: '0 0.5em'\n },\n {\n name: 'verticalAlign',\n str: 'alignment_middle',\n value: 'middle',\n margin: '0 0.5em'\n },\n {\n name: 'verticalAlign',\n str: 'alignment_bottom',\n value: 'text-bottom',\n margin: '0 0.5em',\n isDefault: true\n },\n\n // Floats.\n {\n name: 'float',\n str: 'alignment_left',\n value: 'left',\n margin: '0 0.5em 0 0'\n },\n {\n name: 'float',\n str: 'alignment_right',\n value: 'right',\n margin: '0 0 0 0.5em'\n }\n ];\n\n form = null;\n rawImageDimensions = null;\n canShowFilePicker = true;\n editor = null;\n currentModal = null;\n selectedImage = null;\n\n constructor(editor) {\n this.editor = editor;\n }\n\n displayDialogue() {\n // Reset the image dimensions.\n this.rawImageDimensions = null;\n\n Modal.create({\n type: Modal.types.DEFAULT,\n title: getString('imageproperties', 'tiny_media'),\n body: Templates.render('tiny_media/insert_image', {\n elementid: this.editor.getElement().id,\n CSS: this.CSS,\n FORMNAMES: this.FORMNAMES,\n showfilepicker: this.canShowFilePicker\n })\n }).then(modal => {\n this.currentModal = modal;\n modal.getRoot().on(ModalEvents.bodyRendered, () => {\n this.form = document.querySelector(this.CSS.FORM);\n // Configure the view of the current image.\n this.applyImageProperties();\n this.registerEventListeners();\n });\n modal.getRoot().on(ModalEvents.hidden, () => {\n modal.destroy();\n });\n modal.show();\n return modal;\n });\n }\n\n filePickerCallback(params, self) {\n if (params.url !== '') {\n const input = self.form.querySelector('.' + self.CSS.INPUTURL);\n input.value = params.url;\n\n // Auto set the width and height.\n self.form.querySelector('.' + self.CSS.INPUTWIDTH).value = '';\n self.form.querySelector('.' + self.CSS.INPUTHEIGHT).value = '';\n\n // Load the preview image.\n self.loadPreviewImage(params.url);\n }\n }\n\n loadPreviewImage(url) {\n const image = new Image();\n\n image.onerror = () => {\n const preview = this.form.querySelector('.' + CSS.IMAGEPREVIEW);\n preview.style.display = 'none';\n };\n\n image.onload = () => {\n let input, currentWidth, currentHeight, widthRatio, heightRatio;\n\n // Store dimensions of the raw image, falling back to defaults for images without dimensions (e.g. SVG).\n this.rawImageDimensions = {\n width: image.width || this.DEFAULTS.WIDTH,\n height: image.height || this.DEFAULTS.HEIGHT,\n };\n\n input = this.form.querySelector('.' + this.CSS.INPUTWIDTH);\n currentWidth = input.value;\n if (currentWidth === '') {\n input.value = this.rawImageDimensions.width;\n currentWidth = \"\" + this.rawImageDimensions.width;\n }\n\n input = this.form.querySelector('.' + this.CSS.INPUTHEIGHT);\n currentHeight = input.value;\n if (currentHeight === '') {\n input.value = this.rawImageDimensions.height;\n currentHeight = \"\" + this.rawImageDimensions.height;\n }\n\n input = this.form.querySelector('.' + this.CSS.IMAGEPREVIEW);\n input.setAttribute('src', image.src);\n input.style.display = 'inline';\n\n input = this.form.querySelector('.' + this.CSS.INPUTCONSTRAIN);\n if (currentWidth.match(this.REGEX.ISPERCENT) && currentHeight.match(this.REGEX.ISPERCENT)) {\n input.checked = currentWidth === currentHeight;\n } else if (image.width === 0 || image.height === 0) {\n // If we don't have both dimensions of the image, we can't auto-size it, so disable control.\n input.disabled = 'disabled';\n } else {\n // This is the same as comparing to 3 decimal places.\n widthRatio = Math.round(1000 * parseInt(currentWidth, 10) / image.width);\n heightRatio = Math.round(1000 * parseInt(currentHeight, 10) / image.height);\n input.checked = widthRatio === heightRatio;\n }\n };\n\n image.src = url;\n }\n\n urlChanged() {\n const input = this.form.querySelector('.' + this.CSS.INPUTURL);\n\n if (input.value !== '') {\n // Load the preview image.\n this.loadPreviewImage(input.value);\n }\n }\n\n hasErrorUrlField() {\n const url = this.form.querySelector('.' + this.CSS.INPUTURL).value;\n const urlError = url === '';\n this.toggleVisibility('.' + this.CSS.IMAGEURLWARNING, urlError);\n this.toggleAriaInvalid(['.' + this.CSS.INPUTURL], urlError);\n\n return urlError;\n }\n\n hasErrorAltField() {\n const alt = this.form.querySelector('.' + this.CSS.INPUTALT).value;\n const presentation = this.form.querySelector('.' + this.CSS.IMAGEPRESENTATION).checked;\n const imageAltError = alt === '' && !presentation;\n this.toggleVisibility('.' + this.CSS.IMAGEALTWARNING, imageAltError);\n this.toggleAriaInvalid(['.' + this.CSS.INPUTALT, '.' + this.CSS.IMAGEPRESENTATION], imageAltError);\n\n return imageAltError;\n }\n\n toggleVisibility(selector, predicate) {\n const elements = this.form.querySelectorAll(selector);\n elements.forEach((element) => {\n element.style.display = predicate ? 'block' : 'none';\n });\n }\n\n toggleAriaInvalid(selectors, predicate) {\n selectors.forEach((selector) => {\n const elements = this.form.querySelectorAll(selector);\n elements.forEach((element) => {\n element.setAttribute('aria-invalid', predicate);\n });\n });\n }\n\n getAlignmentClass(alignment) {\n return this.CSS.ALIGNSETTINGS + '_' + alignment;\n }\n\n updateWarning() {\n const urlError = this.hasErrorUrlField();\n const imageAltError = this.hasErrorAltField();\n\n return urlError || imageAltError;\n }\n\n setImage(e) {\n const url = this.form.querySelector('.' + this.CSS.INPUTURL).value,\n alt = this.form.querySelector('.' + this.CSS.INPUTALT).value,\n width = this.form.querySelector('.' + this.CSS.INPUTWIDTH).value,\n height = this.form.querySelector('.' + this.CSS.INPUTHEIGHT).value,\n alignment = this.getAlignmentClass(this.form.querySelector('.' + this.CSS.INPUTALIGNMENT).value),\n presentation = this.form.querySelector('.' + this.CSS.IMAGEPRESENTATION).checked,\n constrain = this.form.querySelector('.' + this.CSS.INPUTCONSTRAIN).value,\n customStyle = this.form.querySelector('.' + this.CSS.INPUTCUSTOMSTYLE).value;\n let imageHtml,\n classList = [];\n\n e.preventDefault();\n\n // Check if there are any accessibility issues.\n if (this.updateWarning()) {\n return;\n }\n\n if (url !== '') {\n if (constrain) {\n classList.push(this.CSS.RESPONSIVE);\n }\n\n // Add the alignment class for the image.\n classList.push(alignment);\n\n if (!width.match(this.REGEX.ISPERCENT) && isNaN(parseInt(width, 10))) {\n this.form.querySelector('.' + this.CSS.INPUTWIDTH).focus();\n return;\n }\n if (!height.match(this.REGEX.ISPERCENT) && isNaN(parseInt(height, 10))) {\n this.form.querySelector('.' + this.CSS.INPUTHEIGHT).focus();\n return;\n }\n\n Templates.render('tiny_media/image', {\n url: url,\n alt: alt,\n width: width,\n height: height,\n presentation: presentation,\n customstyle: customStyle,\n classlist: classList.join(' ')\n }).then(html => {\n imageHtml = html;\n this.editor.insertContent(imageHtml);\n this.currentModal.destroy();\n });\n }\n }\n\n handleKeyupCharacterCount() {\n const alt = this.form.querySelector('.' + this.CSS.INPUTALT).value,\n current = this.form.querySelector('#currentcount');\n current.innerHTML = alt.length;\n }\n\n autoAdjustSize(e, forceHeight) {\n forceHeight = forceHeight || false;\n\n let keyField = this.form.querySelector('.' + this.CSS.INPUTWIDTH),\n keyFieldType = 'width',\n subField = this.form.querySelector('.' + this.CSS.INPUTHEIGHT),\n subFieldType = 'height',\n constrainField = this.form.querySelector('.' + this.CSS.INPUTCONSTRAIN),\n keyFieldValue = keyField.value,\n subFieldValue = subField.value,\n imagePreview = this.form.querySelector('.' + this.CSS.IMAGEPREVIEW),\n rawPercentage,\n rawSize;\n\n // If we do not know the image size, do not do anything.\n if (!this.rawImageDimensions) {\n return;\n }\n\n // Set the width back to default if it is empty.\n if (keyFieldValue === '') {\n keyFieldValue = this.rawImageDimensions[keyFieldType];\n keyField.value = keyFieldValue;\n keyFieldValue = keyField.value;\n }\n\n // Clear the existing preview sizes.\n imagePreview.style.width = null;\n imagePreview.style.height = null;\n\n // Now update with the new values.\n if (!constrainField.checked) {\n // We are not keeping the image proportion - update the preview accordingly.\n\n // Width.\n if (keyFieldValue.match(this.REGEX.ISPERCENT)) {\n rawPercentage = parseInt(keyFieldValue, 10);\n rawSize = this.rawImageDimensions.width / 100 * rawPercentage;\n imagePreview.style.width = rawSize + 'px';\n } else {\n imagePreview.style.width = keyFieldValue + 'px';\n }\n\n // Height.\n if (subFieldValue.match(this.REGEX.ISPERCENT)) {\n rawPercentage = parseInt(subFieldValue, 10);\n rawSize = this.rawImageDimensions.height / 100 * rawPercentage;\n imagePreview.style.height = rawSize + 'px';\n } else {\n imagePreview.style.height = subFieldValue + 'px';\n }\n } else {\n // We are keeping the image in proportion.\n if (forceHeight) {\n // By default we update based on width. Swap the key and sub fields around to achieve a height-based scale.\n let temporaryValue;\n temporaryValue = keyField;\n keyField = subField;\n subField = temporaryValue;\n\n temporaryValue = keyFieldType;\n keyFieldType = subFieldType;\n subFieldType = temporaryValue;\n\n temporaryValue = keyFieldValue;\n keyFieldValue = subFieldValue;\n subFieldValue = temporaryValue;\n }\n\n if (keyFieldValue.match(this.REGEX.ISPERCENT)) {\n // This is a percentage based change. Copy it verbatim.\n subFieldValue = keyFieldValue;\n\n // Set the width to the calculated pixel width.\n rawPercentage = parseInt(keyFieldValue, 10);\n rawSize = this.rawImageDimensions.width / 100 * rawPercentage;\n\n // And apply the width/height to the container.\n imagePreview.style.width = rawSize;\n rawSize = this.rawImageDimensions.height / 100 * rawPercentage;\n imagePreview.style.height = rawSize;\n } else {\n // Calculate the scaled subFieldValue from the keyFieldValue.\n subFieldValue = Math.round((keyFieldValue / this.rawImageDimensions[keyFieldType]) *\n this.rawImageDimensions[subFieldType]);\n\n if (forceHeight) {\n imagePreview.style.width = subFieldValue;\n imagePreview.style.height = keyFieldValue;\n } else {\n imagePreview.style.width = keyFieldValue;\n imagePreview.style.height = subFieldValue;\n }\n }\n\n // Update the subField's value within the form to reflect the changes.\n subField.value = subFieldValue;\n }\n }\n\n applyImageProperties() {\n const properties = this.getSelectedImageProperties(),\n img = this.form.querySelector('.' + this.CSS.IMAGEPREVIEW);\n\n if (properties === false) {\n img.style.display = 'none';\n // Set the default alignment.\n this.ALIGNMENTS.some(alignment => {\n if (alignment.isDefault) {\n this.form.querySelector('.' + this.CSS.INPUTALIGNMENT).value = alignment.value;\n return true;\n }\n\n return false;\n });\n\n return;\n }\n\n if (properties.align) {\n this.form.querySelector('.' + this.CSS.INPUTALIGNMENT).value = properties.align;\n }\n if (properties.customstyle) {\n this.form.querySelector('.' + this.CSS.INPUTCUSTOMSTYLE).value = properties.customstyle;\n }\n if (properties.width) {\n this.form.querySelector('.' + this.CSS.INPUTWIDTH).value = properties.width;\n }\n if (properties.height) {\n this.form.querySelector('.' + this.CSS.INPUTHEIGHT).value = properties.height;\n }\n if (properties.alt) {\n this.form.querySelector('.' + this.CSS.INPUTALT).value = properties.alt;\n }\n if (properties.src) {\n this.form.querySelector('.' + this.CSS.INPUTURL).value = properties.src;\n this.loadPreviewImage(properties.src);\n }\n if (properties.presentation) {\n this.form.querySelector('.' + this.CSS.IMAGEPRESENTATION).checked = 'checked';\n }\n\n // Update the image preview based on the form properties.\n this.autoAdjustSize();\n }\n\n getSelectedImageProperties() {\n let properties = {\n src: null,\n alt: null,\n width: null,\n height: null,\n align: '',\n presentation: false\n },\n\n // Get the current selection.\n image = this.getSelectedImage(),\n width,\n height,\n style;\n\n if (image) {\n image = this.removeLegacyAlignment(image);\n this.selectedImage = image;\n\n style = image.style;\n properties.customstyle = style;\n\n width = image.width;\n if (!String(width).match(this.REGEX.ISPERCENT)) {\n width = parseInt(width, 10);\n }\n height = image.height;\n if (!String(height).match(this.REGEX.ISPERCENT)) {\n height = parseInt(height, 10);\n }\n\n if (width !== 0) {\n properties.width = width;\n }\n if (height !== 0) {\n properties.height = height;\n }\n this.getAlignmentProperties(image, properties);\n properties.src = image.getAttribute('src');\n properties.alt = image.getAttribute('alt') || '';\n properties.presentation = (image.getAttribute('role') === 'presentation');\n return properties;\n }\n\n // No image selected - clean up.\n this.selectedImage = null;\n return false;\n }\n\n removeLegacyAlignment(imageNode) {\n if (!imageNode.style.margin) {\n // There is no margin therefore this cannot match any known alignments.\n return imageNode;\n }\n\n this.ALIGNMENTS.some(alignment => {\n if (imageNode.style[alignment.name] !== alignment.value) {\n // The name/value do not match. Skip.\n return false;\n }\n const normalisedNode = document.createElement('div');\n normalisedNode.style.margin = alignment.margin;\n if (imageNode.style.margin !== normalisedNode.style.margin) {\n // The margin does not match.\n return false;\n }\n\n imageNode.classList.add(this.getAlignmentClass(alignment.value));\n imageNode.style[alignment.name] = null;\n imageNode.style.margin = null;\n\n return true;\n });\n\n return imageNode;\n }\n\n getAlignmentProperties(image, properties) {\n let complete = false,\n defaultAlignment;\n\n // Check for an alignment value.\n complete = this.ALIGNMENTS.some(alignment => {\n const classname = this.getAlignmentClass(alignment.value);\n if (image.classList.contains(classname)) {\n properties.align = alignment.value;\n return true;\n }\n\n if (alignment.isDefault) {\n defaultAlignment = alignment.value;\n }\n\n return false;\n });\n\n if (!complete && defaultAlignment) {\n properties.align = defaultAlignment;\n }\n }\n\n getSelectedImage() {\n const imgElm = this.editor.selection.getNode();\n const figureElm = this.editor.dom.getParent(imgElm, 'figure.image');\n if (figureElm) {\n return this.editor.dom.select('img', figureElm)[0];\n }\n if (imgElm && (imgElm.nodeName !== 'IMG' || this.isPlaceholderImage(imgElm))) {\n return null;\n }\n return imgElm;\n }\n\n isPlaceholderImage(imgElm) {\n return imgElm.nodeName === 'IMG' && (imgElm.hasAttribute('data-mce-object') || imgElm.hasAttribute('data-mce-placeholder'));\n }\n\n registerEventListeners() {\n const self = this;\n this.form.querySelector('.' + this.CSS.INPUTURL).addEventListener('blur', () => {\n this.urlChanged();\n });\n this.form.querySelector('.' + this.CSS.INPUTURL).addEventListener('change', () => {\n this.hasErrorUrlField();\n });\n this.form.querySelector('.' + this.CSS.IMAGEPRESENTATION).addEventListener('change', () => {\n this.hasErrorAltField();\n });\n this.form.querySelector('.' + this.CSS.INPUTALT).addEventListener('blur', () => {\n this.hasErrorAltField();\n });\n this.form.querySelector('.' + this.CSS.INPUTWIDTH).addEventListener('blur', (e) => {\n this.autoAdjustSize(e);\n });\n this.form.querySelector('.' + this.CSS.INPUTHEIGHT).addEventListener('blur', (e) => {\n this.autoAdjustSize(e, true);\n });\n\n this.form.querySelector('.' + this.CSS.INPUTSUBMIT).addEventListener('click', (e) => {\n this.setImage(e);\n });\n if (this.canShowFilePicker) {\n this.form.querySelector('.' + this.CSS.IMAGEBROWSER).addEventListener('click', (e) => {\n e.preventDefault();\n displayFilepicker(this.editor, 'image').then((params) => {\n this.filePickerCallback(params, self);\n }).catch();\n });\n }\n // Character count.\n this.form.querySelector('.' + this.CSS.INPUTALT).addEventListener('keyup', () => {\n this.handleKeyupCharacterCount();\n });\n }\n};\n\n"],"names":["constructor","editor","FORM","RESPONSIVE","INPUTALIGNMENT","INPUTALT","INPUTHEIGHT","INPUTSUBMIT","INPUTURL","INPUTSIZE","INPUTWIDTH","IMAGEALTWARNING","IMAGEURLWARNING","IMAGEBROWSER","IMAGEPRESENTATION","INPUTCONSTRAIN","INPUTCUSTOMSTYLE","IMAGEPREVIEW","IMAGEPREVIEWBOX","ALIGNSETTINGS","URL","ALT","ISPERCENT","WIDTH","HEIGHT","name","str","value","margin","isDefault","displayDialogue","rawImageDimensions","Modal","create","type","types","DEFAULT","title","body","Templates","render","elementid","this","getElement","id","CSS","FORMNAMES","showfilepicker","canShowFilePicker","then","modal","currentModal","getRoot","on","ModalEvents","bodyRendered","form","document","querySelector","applyImageProperties","registerEventListeners","hidden","destroy","show","filePickerCallback","params","self","url","loadPreviewImage","image","Image","onerror","style","display","onload","input","currentWidth","currentHeight","widthRatio","heightRatio","width","DEFAULTS","height","setAttribute","src","match","REGEX","checked","disabled","Math","round","parseInt","urlChanged","hasErrorUrlField","urlError","toggleVisibility","toggleAriaInvalid","hasErrorAltField","alt","presentation","imageAltError","selector","predicate","querySelectorAll","forEach","element","selectors","getAlignmentClass","alignment","updateWarning","setImage","e","constrain","customStyle","imageHtml","classList","preventDefault","push","isNaN","focus","customstyle","classlist","join","html","insertContent","handleKeyupCharacterCount","innerHTML","length","autoAdjustSize","forceHeight","rawPercentage","rawSize","keyField","keyFieldType","subField","subFieldType","constrainField","keyFieldValue","subFieldValue","imagePreview","temporaryValue","properties","getSelectedImageProperties","img","ALIGNMENTS","some","align","getSelectedImage","removeLegacyAlignment","selectedImage","String","getAlignmentProperties","getAttribute","imageNode","normalisedNode","createElement","add","defaultAlignment","complete","classname","contains","imgElm","selection","getNode","figureElm","dom","getParent","select","nodeName","isPlaceholderImage","hasAttribute","addEventListener","catch"],"mappings":"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"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/media/amd/build/plugin.min.js b/lib/editor/tiny/plugins/media/amd/build/plugin.min.js new file mode 100644 index 0000000000000..0b11687916e8e --- /dev/null +++ b/lib/editor/tiny/plugins/media/amd/build/plugin.min.js @@ -0,0 +1,10 @@ +define("tiny_media/plugin",["exports","editor_tiny/loader","editor_tiny/utils","./common","./commands","./configuration"],(function(_exports,_loader,_utils,_common,Commands,Configuration){function _getRequireWildcardCache(nodeInterop){if("function"!=typeof WeakMap)return null;var cacheBabelInterop=new WeakMap,cacheNodeInterop=new WeakMap;return(_getRequireWildcardCache=function(nodeInterop){return nodeInterop?cacheNodeInterop:cacheBabelInterop})(nodeInterop)}function _interopRequireWildcard(obj,nodeInterop){if(!nodeInterop&&obj&&obj.__esModule)return obj;if(null===obj||"object"!=typeof obj&&"function"!=typeof obj)return{default:obj};var cache=_getRequireWildcardCache(nodeInterop);if(cache&&cache.has(obj))return cache.get(obj);var newObj={},hasPropertyDescriptor=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var key in obj)if("default"!==key&&Object.prototype.hasOwnProperty.call(obj,key)){var desc=hasPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):null;desc&&(desc.get||desc.set)?Object.defineProperty(newObj,key,desc):newObj[key]=obj[key]}return newObj.default=obj,cache&&cache.set(obj,newObj),newObj} +/** + * Tiny Media plugin for Moodle. + * + * @module tiny_media/plugin + * @copyright 2022 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,Commands=_interopRequireWildcard(Commands),Configuration=_interopRequireWildcard(Configuration);var _default=new Promise((async resolve=>{const[tinyMCE,setupCommands,pluginMetadata]=await Promise.all([(0,_loader.getTinyMCE)(),Commands.getSetup(),(0,_utils.getPluginMetadata)(_common.component,_common.pluginName)]);tinyMCE.PluginManager.add("".concat(_common.component,"/plugin"),(editor=>(setupCommands(editor),pluginMetadata))),resolve(["".concat(_common.component,"/plugin"),Configuration])}));return _exports.default=_default,_exports.default})); + +//# sourceMappingURL=plugin.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/media/amd/build/plugin.min.js.map b/lib/editor/tiny/plugins/media/amd/build/plugin.min.js.map new file mode 100644 index 0000000000000..1df793032515b --- /dev/null +++ b/lib/editor/tiny/plugins/media/amd/build/plugin.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"plugin.min.js","sources":["../src/plugin.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Tiny Media plugin for Moodle.\n *\n * @module tiny_media/plugin\n * @copyright 2022 Andrew Lyons \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport {getTinyMCE} from 'editor_tiny/loader';\nimport {getPluginMetadata} from 'editor_tiny/utils';\n\nimport {component, pluginName} from './common';\nimport * as Commands from './commands';\nimport * as Configuration from './configuration';\n\nexport default new Promise(async(resolve) => {\n const [\n tinyMCE,\n setupCommands,\n pluginMetadata,\n ] = await Promise.all([\n getTinyMCE(),\n Commands.getSetup(),\n getPluginMetadata(component, pluginName),\n ]);\n\n tinyMCE.PluginManager.add(`${component}/plugin`, (editor) => {\n // Setup the Commands (buttons, menu items, and so on).\n setupCommands(editor);\n\n return pluginMetadata;\n });\n\n // Resolve the Media Plugin and include configuration.\n resolve([`${component}/plugin`, Configuration]);\n});\n"],"names":["Promise","async","tinyMCE","setupCommands","pluginMetadata","all","Commands","getSetup","component","pluginName","PluginManager","add","editor","resolve","Configuration"],"mappings":";;;;;;;kMA6Be,IAAIA,SAAQC,MAAAA,gBAEnBC,QACAC,cACAC,sBACMJ,QAAQK,IAAI,EAClB,wBACAC,SAASC,YACT,4BAAkBC,kBAAWC,sBAGjCP,QAAQQ,cAAcC,cAAOH,8BAAqBI,SAE9CT,cAAcS,QAEPR,kBAIXS,QAAQ,WAAIL,6BAAoBM"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/media/amd/build/upload-handler.min.js b/lib/editor/tiny/plugins/media/amd/build/upload-handler.min.js new file mode 100644 index 0000000000000..94e7d24b07ece --- /dev/null +++ b/lib/editor/tiny/plugins/media/amd/build/upload-handler.min.js @@ -0,0 +1,3 @@ +define("tiny_media/upload-handler",["exports","core_form/events","editor_tiny/options"],(function(_exports,_events,_options){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0;return _exports.default=(activeEditor,blobInfo,progress)=>new Promise(((resolve,reject)=>{var _options$savepath;(0,_events.notifyUploadStarted)(activeEditor.targetElm.id);const xhr=new XMLHttpRequest;xhr.upload.addEventListener("progress",(e=>{progress(e.loaded/e.total*100)})),xhr.addEventListener("load",(()=>{if(403===xhr.status)return void reject({message:"HTTP error: ".concat(xhr.status),remove:!0});if(xhr.status<200||xhr.status>=300)return void reject("HTTP Error: ".concat(xhr.status));const response=JSON.parse(xhr.responseText);if(!response)return void reject("Invalid JSON: ".concat(xhr.responseText));let location;(0,_events.notifyUploadCompleted)(activeEditor.targetElm.id),response.url?location=response.url:response.event&&"fileexists"===response.event&&response.newfile&&(location=response.newfile.url),location&&"string"==typeof location?resolve(location):reject("Unable to handle file result: ".concat(xhr.responseText))})),xhr.addEventListener("error",(()=>{reject("Image upload failed due to an XHR transport error. Code: ".concat(xhr.status))}));const formData=new FormData,options=(0,_options.getFilePicker)(activeEditor,"image");formData.append("repo_upload_file",blobInfo.blob()),formData.append("itemid",options.itemid),Object.values(options.repositories).some((repository=>"upload"===repository.type&&(formData.append("repo_id",repository.id),!0))),formData.append("env",options.env),formData.append("sesskey",M.cfg.sesskey),formData.append("client_id",options.client_id),formData.append("savepath",null!==(_options$savepath=options.savepath)&&void 0!==_options$savepath?_options$savepath:"/"),formData.append("ctx_id",options.context.id),xhr.open("POST","".concat(M.cfg.wwwroot,"/repository/repository_ajax.php?action=upload"),!0),xhr.send(formData)})),_exports.default})); + +//# sourceMappingURL=upload-handler.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/media/amd/build/upload-handler.min.js.map b/lib/editor/tiny/plugins/media/amd/build/upload-handler.min.js.map new file mode 100644 index 0000000000000..be56eddee9aad --- /dev/null +++ b/lib/editor/tiny/plugins/media/amd/build/upload-handler.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"upload-handler.min.js","sources":["../src/upload-handler.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Tiny Media plugin for Moodle.\n *\n * @module tiny_media/plugin\n * @copyright 2022 Andrew Lyons \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\nimport {\n notifyUploadStarted,\n notifyUploadCompleted,\n} from 'core_form/events';\nimport {getFilePicker} from 'editor_tiny/options';\n\n// This image uploader is based on advice given at:\n// https://www.tiny.cloud/docs/tinymce/6/upload-images/\nexport default (activeEditor, blobInfo, progress) => new Promise((resolve, reject) => {\n notifyUploadStarted(activeEditor.targetElm.id);\n debugger; // eslint-disable-line\n\n const xhr = new XMLHttpRequest();\n\n // Add the progress handler.\n xhr.upload.addEventListener('progress', (e) => {\n progress(e.loaded / e.total * 100);\n });\n\n xhr.addEventListener('load', () => {\n if (xhr.status === 403) {\n reject({\n message: `HTTP error: ${xhr.status}`,\n remove: true,\n });\n return;\n }\n\n if (xhr.status < 200 || xhr.status >= 300) {\n reject(`HTTP Error: ${xhr.status}`);\n return;\n }\n\n const response = JSON.parse(xhr.responseText);\n\n if (!response) {\n reject(`Invalid JSON: ${xhr.responseText}`);\n return;\n }\n\n notifyUploadCompleted(activeEditor.targetElm.id);\n\n let location;\n if (response.url) {\n location = response.url;\n } else if (response.event && response.event === 'fileexists' && response.newfile) {\n // A file with this name is already in use here - rename to avoid conflict.\n // Chances are, it's a different image (stored in a different folder on the user's computer).\n // If the user wants to reuse an existing image, they can copy/paste it within the editor.\n location = response.newfile.url;\n }\n\n if (location && typeof location === 'string') {\n resolve(location);\n return;\n }\n\n reject(`Unable to handle file result: ${xhr.responseText}`);\n });\n\n xhr.addEventListener('error', () => {\n reject(`Image upload failed due to an XHR transport error. Code: ${xhr.status}`);\n });\n\n const formData = new FormData();\n const options = getFilePicker(activeEditor, 'image');\n\n formData.append('repo_upload_file', blobInfo.blob());\n formData.append('itemid', options.itemid);\n Object.values(options.repositories).some((repository) => {\n if (repository.type === 'upload') {\n formData.append('repo_id', repository.id);\n return true;\n }\n return false;\n });\n\n formData.append('env', options.env);\n formData.append('sesskey', M.cfg.sesskey);\n formData.append('client_id', options.client_id);\n formData.append('savepath', options.savepath ?? '/');\n formData.append('ctx_id', options.context.id);\n\n xhr.open('POST', `${M.cfg.wwwroot}/repository/repository_ajax.php?action=upload`, true);\n xhr.send(formData);\n});\n"],"names":["activeEditor","blobInfo","progress","Promise","resolve","reject","targetElm","id","xhr","XMLHttpRequest","upload","addEventListener","e","loaded","total","status","message","remove","response","JSON","parse","responseText","location","url","event","newfile","formData","FormData","options","append","blob","itemid","Object","values","repositories","some","repository","type","env","M","cfg","sesskey","client_id","savepath","context","open","wwwroot","send"],"mappings":"qOA8Be,CAACA,aAAcC,SAAUC,WAAa,IAAIC,SAAQ,CAACC,QAASC,gEACnDL,aAAaM,UAAUC,UAGrCC,IAAM,IAAIC,eAGhBD,IAAIE,OAAOC,iBAAiB,YAAaC,IACrCV,SAASU,EAAEC,OAASD,EAAEE,MAAQ,QAGlCN,IAAIG,iBAAiB,QAAQ,QACN,MAAfH,IAAIO,mBACJV,OAAO,CACHW,8BAAwBR,IAAIO,QAC5BE,QAAQ,OAKZT,IAAIO,OAAS,KAAOP,IAAIO,QAAU,gBAClCV,6BAAsBG,IAAIO,eAIxBG,SAAWC,KAAKC,MAAMZ,IAAIa,kBAE3BH,qBACDb,+BAAwBG,IAAIa,mBAM5BC,2CAFkBtB,aAAaM,UAAUC,IAGzCW,SAASK,IACTD,SAAWJ,SAASK,IACbL,SAASM,OAA4B,eAAnBN,SAASM,OAA0BN,SAASO,UAIrEH,SAAWJ,SAASO,QAAQF,KAG5BD,UAAgC,iBAAbA,SACnBlB,QAAQkB,UAIZjB,+CAAwCG,IAAIa,kBAGhDb,IAAIG,iBAAiB,SAAS,KAC1BN,0EAAmEG,IAAIO,kBAGrEW,SAAW,IAAIC,SACfC,SAAU,0BAAc5B,aAAc,SAE5C0B,SAASG,OAAO,mBAAoB5B,SAAS6B,QAC7CJ,SAASG,OAAO,SAAUD,QAAQG,QAClCC,OAAOC,OAAOL,QAAQM,cAAcC,MAAMC,YACd,WAApBA,WAAWC,OACXX,SAASG,OAAO,UAAWO,WAAW7B,KAC/B,KAKfmB,SAASG,OAAO,MAAOD,QAAQU,KAC/BZ,SAASG,OAAO,UAAWU,EAAEC,IAAIC,SACjCf,SAASG,OAAO,YAAaD,QAAQc,WACrChB,SAASG,OAAO,qCAAYD,QAAQe,wDAAY,KAChDjB,SAASG,OAAO,SAAUD,QAAQgB,QAAQrC,IAE1CC,IAAIqC,KAAK,iBAAWN,EAAEC,IAAIM,0DAAwD,GAClFtC,IAAIuC,KAAKrB"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/media/amd/src/commands.js b/lib/editor/tiny/plugins/media/amd/src/commands.js new file mode 100644 index 0000000000000..bf20bba509d42 --- /dev/null +++ b/lib/editor/tiny/plugins/media/amd/src/commands.js @@ -0,0 +1,72 @@ +// 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 . + +/** + * Tiny Media commands. + * + * @module tiny_media/commands + * @copyright 2022 Huong Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {get_string as getString} from 'core/str'; +import { + component, + imageButtonName, +} from './common'; +import {MediaImage} from './image'; + +export const getSetup = async() => { + const [ + imageButtonText, + ] = await Promise.all([ + getString('imagebuttontitle', component), + ]); + + return (editor) => { + const mediaImage = new MediaImage(editor); + const icon = 'image'; + + // Register the Menu Button as a toggle. + // This means that when highlighted over an existing Media Image element it will show as toggled on. + editor.ui.registry.addToggleButton(imageButtonName, { + icon, + text: imageButtonText, + tooltip: imageButtonText, + onAction: () => {mediaImage.displayDialogue();}, + onSetup: api => { + return editor.selection.selectorChangedWithUnbind( + 'img:not([data-mce-object]):not([data-mce-placeholder]),figure.image', + api.setActive + ).unbind; + } + }); + + editor.ui.registry.addMenuItem(imageButtonName, { + icon, + text: imageButtonText, + onAction: () => {mediaImage.displayDialogue();} + }); + + editor.ui.registry.addContextToolbar(imageButtonName, { + predicate: node => { + return node.nodeName.toLowerCase() === 'img'; + }, + items: imageButtonName, + position: 'node', + scope: 'node' + }); + }; +}; diff --git a/lib/editor/tiny/plugins/media/amd/src/common.js b/lib/editor/tiny/plugins/media/amd/src/common.js new file mode 100644 index 0000000000000..937f6a6267721 --- /dev/null +++ b/lib/editor/tiny/plugins/media/amd/src/common.js @@ -0,0 +1,28 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Tiny Media common values. + * + * @module tiny_media/common + * @copyright 2022 Huong Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +export default { + pluginName: 'tiny_media/plugin', + component: 'tiny_media', + imageButtonName: 'tiny_media', +}; diff --git a/lib/editor/tiny/plugins/media/amd/src/configuration.js b/lib/editor/tiny/plugins/media/amd/src/configuration.js new file mode 100644 index 0000000000000..f458690b44139 --- /dev/null +++ b/lib/editor/tiny/plugins/media/amd/src/configuration.js @@ -0,0 +1,43 @@ +// 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 . + +/** + * Tiny Media configuration. + * + * @module tiny_media/configuration + * @copyright 2022 Huong Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import { + imageButtonName, +} from './common'; +import uploadHandler from "./upload-handler"; +import { + addContextmenuItem, + addMenubarItem, + addToolbarButton, +} from 'editor_tiny/utils'; + +export const configure = (instanceConfig) => { + // Update the instance configuration to add the Media menu option to the menus and toolbars and upload_handler. + return { + contextmenu: addContextmenuItem(instanceConfig.contextmenu, imageButtonName), + toolbar: addToolbarButton(instanceConfig.toolbar, 'content', [imageButtonName]), + menu: addMenubarItem(instanceConfig.menu, 'insert', imageButtonName), + quickbars_insert_toolbar: addContextmenuItem(instanceConfig.quickbars_insert_toolbar, imageButtonName), + images_upload_handler: (blobInfo, progress) => uploadHandler(window.tinymce.activeEditor, blobInfo, progress) + }; +}; diff --git a/lib/editor/tiny/plugins/media/amd/src/image.js b/lib/editor/tiny/plugins/media/amd/src/image.js new file mode 100644 index 0000000000000..b2a4c1f692596 --- /dev/null +++ b/lib/editor/tiny/plugins/media/amd/src/image.js @@ -0,0 +1,625 @@ +// 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 . + +/** + * Tiny Media plugin Image class for Moodle. + * + * @module tiny_media/image + * @copyright 2022 Huong Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Templates from 'core/templates'; +import {get_string as getString} from 'core/str'; +import * as Modal from 'core/modal_factory'; +import * as ModalEvents from 'core/modal_events'; +import {displayFilepicker} from 'editor_tiny/utils'; + +export const MediaImage = class { + + CSS = { + FORM: 'form.tiny_image_form', + RESPONSIVE: 'img-fluid', + INPUTALIGNMENT: 'tiny_image_alignment', + INPUTALT: 'tiny_image_altentry', + INPUTHEIGHT: 'tiny_image_heightentry', + INPUTSUBMIT: 'tiny_image_urlentrysubmit', + INPUTURL: 'tiny_image_urlentry', + INPUTSIZE: 'tiny_image_size', + INPUTWIDTH: 'tiny_image_widthentry', + IMAGEALTWARNING: 'tiny_image_altwarning', + IMAGEURLWARNING: 'tiny_image_urlwarning', + IMAGEBROWSER: 'openimagebrowser', + IMAGEPRESENTATION: 'tiny_image_presentation', + INPUTCONSTRAIN: 'tiny_image_constrain', + INPUTCUSTOMSTYLE: 'tiny_image_customstyle', + IMAGEPREVIEW: 'tiny_image_preview', + IMAGEPREVIEWBOX: 'tiny_image_preview_box', + ALIGNSETTINGS: 'tiny_image_button' + }; + + FORMNAMES = { + URL: 'urlentry', + ALT: 'altentry' + }; + + REGEX = { + ISPERCENT: /\d+%/ + }; + + DEFAULTS = { + WIDTH: 160, + HEIGHT: 160, + }; + + ALIGNMENTS = [ + // Vertical alignment. + { + name: 'verticalAlign', + str: 'alignment_top', + value: 'text-top', + margin: '0 0.5em' + }, + { + name: 'verticalAlign', + str: 'alignment_middle', + value: 'middle', + margin: '0 0.5em' + }, + { + name: 'verticalAlign', + str: 'alignment_bottom', + value: 'text-bottom', + margin: '0 0.5em', + isDefault: true + }, + + // Floats. + { + name: 'float', + str: 'alignment_left', + value: 'left', + margin: '0 0.5em 0 0' + }, + { + name: 'float', + str: 'alignment_right', + value: 'right', + margin: '0 0 0 0.5em' + } + ]; + + form = null; + rawImageDimensions = null; + canShowFilePicker = true; + editor = null; + currentModal = null; + selectedImage = null; + + constructor(editor) { + this.editor = editor; + } + + displayDialogue() { + // Reset the image dimensions. + this.rawImageDimensions = null; + + Modal.create({ + type: Modal.types.DEFAULT, + title: getString('imageproperties', 'tiny_media'), + body: Templates.render('tiny_media/insert_image', { + elementid: this.editor.getElement().id, + CSS: this.CSS, + FORMNAMES: this.FORMNAMES, + showfilepicker: this.canShowFilePicker + }) + }).then(modal => { + this.currentModal = modal; + modal.getRoot().on(ModalEvents.bodyRendered, () => { + this.form = document.querySelector(this.CSS.FORM); + // Configure the view of the current image. + this.applyImageProperties(); + this.registerEventListeners(); + }); + modal.getRoot().on(ModalEvents.hidden, () => { + modal.destroy(); + }); + modal.show(); + return modal; + }); + } + + filePickerCallback(params, self) { + if (params.url !== '') { + const input = self.form.querySelector('.' + self.CSS.INPUTURL); + input.value = params.url; + + // Auto set the width and height. + self.form.querySelector('.' + self.CSS.INPUTWIDTH).value = ''; + self.form.querySelector('.' + self.CSS.INPUTHEIGHT).value = ''; + + // Load the preview image. + self.loadPreviewImage(params.url); + } + } + + loadPreviewImage(url) { + const image = new Image(); + + image.onerror = () => { + const preview = this.form.querySelector('.' + CSS.IMAGEPREVIEW); + preview.style.display = 'none'; + }; + + image.onload = () => { + let input, currentWidth, currentHeight, widthRatio, heightRatio; + + // 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, + }; + + input = this.form.querySelector('.' + this.CSS.INPUTWIDTH); + currentWidth = input.value; + if (currentWidth === '') { + input.value = this.rawImageDimensions.width; + currentWidth = "" + this.rawImageDimensions.width; + } + + input = this.form.querySelector('.' + this.CSS.INPUTHEIGHT); + currentHeight = input.value; + if (currentHeight === '') { + input.value = this.rawImageDimensions.height; + currentHeight = "" + this.rawImageDimensions.height; + } + + input = this.form.querySelector('.' + this.CSS.IMAGEPREVIEW); + input.setAttribute('src', image.src); + input.style.display = 'inline'; + + input = this.form.querySelector('.' + this.CSS.INPUTCONSTRAIN); + if (currentWidth.match(this.REGEX.ISPERCENT) && currentHeight.match(this.REGEX.ISPERCENT)) { + input.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. + input.disabled = 'disabled'; + } else { + // This is the same as comparing to 3 decimal places. + widthRatio = Math.round(1000 * parseInt(currentWidth, 10) / image.width); + heightRatio = Math.round(1000 * parseInt(currentHeight, 10) / image.height); + input.checked = widthRatio === heightRatio; + } + }; + + image.src = url; + } + + urlChanged() { + const input = this.form.querySelector('.' + this.CSS.INPUTURL); + + if (input.value !== '') { + // Load the preview image. + this.loadPreviewImage(input.value); + } + } + + hasErrorUrlField() { + const url = this.form.querySelector('.' + this.CSS.INPUTURL).value; + const urlError = url === ''; + this.toggleVisibility('.' + this.CSS.IMAGEURLWARNING, urlError); + this.toggleAriaInvalid(['.' + this.CSS.INPUTURL], urlError); + + return urlError; + } + + hasErrorAltField() { + const alt = this.form.querySelector('.' + this.CSS.INPUTALT).value; + const presentation = this.form.querySelector('.' + this.CSS.IMAGEPRESENTATION).checked; + const imageAltError = alt === '' && !presentation; + this.toggleVisibility('.' + this.CSS.IMAGEALTWARNING, imageAltError); + this.toggleAriaInvalid(['.' + this.CSS.INPUTALT, '.' + this.CSS.IMAGEPRESENTATION], imageAltError); + + return imageAltError; + } + + toggleVisibility(selector, predicate) { + const elements = this.form.querySelectorAll(selector); + elements.forEach((element) => { + element.style.display = predicate ? 'block' : 'none'; + }); + } + + toggleAriaInvalid(selectors, predicate) { + selectors.forEach((selector) => { + const elements = this.form.querySelectorAll(selector); + elements.forEach((element) => { + element.setAttribute('aria-invalid', predicate); + }); + }); + } + + getAlignmentClass(alignment) { + return this.CSS.ALIGNSETTINGS + '_' + alignment; + } + + updateWarning() { + const urlError = this.hasErrorUrlField(); + const imageAltError = this.hasErrorAltField(); + + return urlError || imageAltError; + } + + setImage(e) { + const url = this.form.querySelector('.' + this.CSS.INPUTURL).value, + alt = this.form.querySelector('.' + this.CSS.INPUTALT).value, + width = this.form.querySelector('.' + this.CSS.INPUTWIDTH).value, + height = this.form.querySelector('.' + this.CSS.INPUTHEIGHT).value, + alignment = this.getAlignmentClass(this.form.querySelector('.' + this.CSS.INPUTALIGNMENT).value), + presentation = this.form.querySelector('.' + this.CSS.IMAGEPRESENTATION).checked, + constrain = this.form.querySelector('.' + this.CSS.INPUTCONSTRAIN).value, + customStyle = this.form.querySelector('.' + this.CSS.INPUTCUSTOMSTYLE).value; + let imageHtml, + classList = []; + + e.preventDefault(); + + // Check if there are any accessibility issues. + if (this.updateWarning()) { + return; + } + + if (url !== '') { + if (constrain) { + classList.push(this.CSS.RESPONSIVE); + } + + // Add the alignment class for the image. + classList.push(alignment); + + if (!width.match(this.REGEX.ISPERCENT) && isNaN(parseInt(width, 10))) { + this.form.querySelector('.' + this.CSS.INPUTWIDTH).focus(); + return; + } + if (!height.match(this.REGEX.ISPERCENT) && isNaN(parseInt(height, 10))) { + this.form.querySelector('.' + this.CSS.INPUTHEIGHT).focus(); + return; + } + + Templates.render('tiny_media/image', { + url: url, + alt: alt, + width: width, + height: height, + presentation: presentation, + customstyle: customStyle, + classlist: classList.join(' ') + }).then(html => { + imageHtml = html; + this.editor.insertContent(imageHtml); + this.currentModal.destroy(); + }); + } + } + + handleKeyupCharacterCount() { + const alt = this.form.querySelector('.' + this.CSS.INPUTALT).value, + current = this.form.querySelector('#currentcount'); + current.innerHTML = alt.length; + } + + autoAdjustSize(e, forceHeight) { + forceHeight = forceHeight || false; + + let keyField = this.form.querySelector('.' + this.CSS.INPUTWIDTH), + keyFieldType = 'width', + subField = this.form.querySelector('.' + this.CSS.INPUTHEIGHT), + subFieldType = 'height', + constrainField = this.form.querySelector('.' + this.CSS.INPUTCONSTRAIN), + keyFieldValue = keyField.value, + subFieldValue = subField.value, + imagePreview = this.form.querySelector('.' + this.CSS.IMAGEPREVIEW), + rawPercentage, + rawSize; + + // If we do not know the image size, do not do anything. + if (!this.rawImageDimensions) { + return; + } + + // Set the width back to default if it is empty. + if (keyFieldValue === '') { + keyFieldValue = this.rawImageDimensions[keyFieldType]; + keyField.value = keyFieldValue; + keyFieldValue = keyField.value; + } + + // Clear the existing preview sizes. + imagePreview.style.width = null; + imagePreview.style.height = null; + + // Now update with the new values. + if (!constrainField.checked) { + // We are not keeping the image proportion - update the preview accordingly. + + // Width. + if (keyFieldValue.match(this.REGEX.ISPERCENT)) { + rawPercentage = parseInt(keyFieldValue, 10); + rawSize = this.rawImageDimensions.width / 100 * rawPercentage; + imagePreview.style.width = rawSize + 'px'; + } else { + imagePreview.style.width = keyFieldValue + 'px'; + } + + // Height. + if (subFieldValue.match(this.REGEX.ISPERCENT)) { + rawPercentage = parseInt(subFieldValue, 10); + rawSize = this.rawImageDimensions.height / 100 * rawPercentage; + imagePreview.style.height = rawSize + 'px'; + } else { + imagePreview.style.height = subFieldValue + 'px'; + } + } else { + // We are keeping the image in proportion. + if (forceHeight) { + // By default we update based on width. Swap the key and sub fields around to achieve a height-based scale. + let temporaryValue; + temporaryValue = keyField; + keyField = subField; + subField = temporaryValue; + + temporaryValue = keyFieldType; + keyFieldType = subFieldType; + subFieldType = temporaryValue; + + temporaryValue = keyFieldValue; + keyFieldValue = subFieldValue; + subFieldValue = temporaryValue; + } + + if (keyFieldValue.match(this.REGEX.ISPERCENT)) { + // This is a percentage based change. Copy it verbatim. + subFieldValue = keyFieldValue; + + // Set the width to the calculated pixel width. + rawPercentage = parseInt(keyFieldValue, 10); + rawSize = this.rawImageDimensions.width / 100 * rawPercentage; + + // And apply the width/height to the container. + imagePreview.style.width = rawSize; + rawSize = this.rawImageDimensions.height / 100 * rawPercentage; + imagePreview.style.height = rawSize; + } else { + // Calculate the scaled subFieldValue from the keyFieldValue. + subFieldValue = Math.round((keyFieldValue / this.rawImageDimensions[keyFieldType]) * + this.rawImageDimensions[subFieldType]); + + if (forceHeight) { + imagePreview.style.width = subFieldValue; + imagePreview.style.height = keyFieldValue; + } else { + imagePreview.style.width = keyFieldValue; + imagePreview.style.height = subFieldValue; + } + } + + // Update the subField's value within the form to reflect the changes. + subField.value = subFieldValue; + } + } + + applyImageProperties() { + const properties = this.getSelectedImageProperties(), + img = this.form.querySelector('.' + this.CSS.IMAGEPREVIEW); + + if (properties === false) { + img.style.display = 'none'; + // Set the default alignment. + this.ALIGNMENTS.some(alignment => { + if (alignment.isDefault) { + this.form.querySelector('.' + this.CSS.INPUTALIGNMENT).value = alignment.value; + return true; + } + + return false; + }); + + return; + } + + if (properties.align) { + this.form.querySelector('.' + this.CSS.INPUTALIGNMENT).value = properties.align; + } + if (properties.customstyle) { + this.form.querySelector('.' + this.CSS.INPUTCUSTOMSTYLE).value = properties.customstyle; + } + if (properties.width) { + this.form.querySelector('.' + this.CSS.INPUTWIDTH).value = properties.width; + } + if (properties.height) { + this.form.querySelector('.' + this.CSS.INPUTHEIGHT).value = properties.height; + } + if (properties.alt) { + this.form.querySelector('.' + this.CSS.INPUTALT).value = properties.alt; + } + if (properties.src) { + this.form.querySelector('.' + this.CSS.INPUTURL).value = properties.src; + this.loadPreviewImage(properties.src); + } + if (properties.presentation) { + this.form.querySelector('.' + this.CSS.IMAGEPRESENTATION).checked = 'checked'; + } + + // Update the image preview based on the form properties. + this.autoAdjustSize(); + } + + getSelectedImageProperties() { + let properties = { + src: null, + alt: null, + width: null, + height: null, + align: '', + presentation: false + }, + + // Get the current selection. + image = this.getSelectedImage(), + width, + height, + style; + + if (image) { + image = this.removeLegacyAlignment(image); + this.selectedImage = image; + + style = image.style; + properties.customstyle = style; + + width = image.width; + if (!String(width).match(this.REGEX.ISPERCENT)) { + width = parseInt(width, 10); + } + height = image.height; + if (!String(height).match(this.REGEX.ISPERCENT)) { + height = parseInt(height, 10); + } + + if (width !== 0) { + properties.width = width; + } + if (height !== 0) { + properties.height = height; + } + this.getAlignmentProperties(image, properties); + properties.src = image.getAttribute('src'); + properties.alt = image.getAttribute('alt') || ''; + properties.presentation = (image.getAttribute('role') === 'presentation'); + return properties; + } + + // No image selected - clean up. + this.selectedImage = null; + return false; + } + + removeLegacyAlignment(imageNode) { + if (!imageNode.style.margin) { + // There is no margin therefore this cannot match any known alignments. + return imageNode; + } + + this.ALIGNMENTS.some(alignment => { + if (imageNode.style[alignment.name] !== alignment.value) { + // The name/value do not match. Skip. + return false; + } + const normalisedNode = document.createElement('div'); + normalisedNode.style.margin = alignment.margin; + if (imageNode.style.margin !== normalisedNode.style.margin) { + // The margin does not match. + return false; + } + + imageNode.classList.add(this.getAlignmentClass(alignment.value)); + imageNode.style[alignment.name] = null; + imageNode.style.margin = null; + + return true; + }); + + return imageNode; + } + + getAlignmentProperties(image, properties) { + let complete = false, + defaultAlignment; + + // Check for an alignment value. + complete = this.ALIGNMENTS.some(alignment => { + const classname = this.getAlignmentClass(alignment.value); + if (image.classList.contains(classname)) { + properties.align = alignment.value; + return true; + } + + if (alignment.isDefault) { + defaultAlignment = alignment.value; + } + + return false; + }); + + if (!complete && defaultAlignment) { + properties.align = defaultAlignment; + } + } + + 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 !== 'IMG' || this.isPlaceholderImage(imgElm))) { + return null; + } + return imgElm; + } + + isPlaceholderImage(imgElm) { + return imgElm.nodeName === 'IMG' && (imgElm.hasAttribute('data-mce-object') || imgElm.hasAttribute('data-mce-placeholder')); + } + + registerEventListeners() { + const self = this; + this.form.querySelector('.' + this.CSS.INPUTURL).addEventListener('blur', () => { + this.urlChanged(); + }); + this.form.querySelector('.' + this.CSS.INPUTURL).addEventListener('change', () => { + this.hasErrorUrlField(); + }); + this.form.querySelector('.' + this.CSS.IMAGEPRESENTATION).addEventListener('change', () => { + this.hasErrorAltField(); + }); + this.form.querySelector('.' + this.CSS.INPUTALT).addEventListener('blur', () => { + this.hasErrorAltField(); + }); + this.form.querySelector('.' + this.CSS.INPUTWIDTH).addEventListener('blur', (e) => { + this.autoAdjustSize(e); + }); + this.form.querySelector('.' + this.CSS.INPUTHEIGHT).addEventListener('blur', (e) => { + this.autoAdjustSize(e, true); + }); + + this.form.querySelector('.' + this.CSS.INPUTSUBMIT).addEventListener('click', (e) => { + this.setImage(e); + }); + if (this.canShowFilePicker) { + this.form.querySelector('.' + this.CSS.IMAGEBROWSER).addEventListener('click', (e) => { + e.preventDefault(); + displayFilepicker(this.editor, 'image').then((params) => { + this.filePickerCallback(params, self); + }).catch(); + }); + } + // Character count. + this.form.querySelector('.' + this.CSS.INPUTALT).addEventListener('keyup', () => { + this.handleKeyupCharacterCount(); + }); + } +}; + diff --git a/lib/editor/tiny/plugins/media/amd/src/plugin.js b/lib/editor/tiny/plugins/media/amd/src/plugin.js new file mode 100644 index 0000000000000..77ddc4dd8d985 --- /dev/null +++ b/lib/editor/tiny/plugins/media/amd/src/plugin.js @@ -0,0 +1,50 @@ +// 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 . + +/** + * Tiny Media plugin for Moodle. + * + * @module tiny_media/plugin + * @copyright 2022 Andrew Lyons + * @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'; + +export default new Promise(async(resolve) => { + const [ + tinyMCE, + setupCommands, + pluginMetadata, + ] = await Promise.all([ + getTinyMCE(), + Commands.getSetup(), + getPluginMetadata(component, pluginName), + ]); + + tinyMCE.PluginManager.add(`${component}/plugin`, (editor) => { + // Setup the Commands (buttons, menu items, and so on). + setupCommands(editor); + + return pluginMetadata; + }); + + // Resolve the Media Plugin and include configuration. + resolve([`${component}/plugin`, Configuration]); +}); diff --git a/lib/editor/tiny/plugins/media/amd/src/upload-handler.js b/lib/editor/tiny/plugins/media/amd/src/upload-handler.js new file mode 100644 index 0000000000000..e3cfd9c7dc1db --- /dev/null +++ b/lib/editor/tiny/plugins/media/amd/src/upload-handler.js @@ -0,0 +1,108 @@ +// 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 . + +/** + * Tiny Media plugin for Moodle. + * + * @module tiny_media/plugin + * @copyright 2022 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +import { + notifyUploadStarted, + notifyUploadCompleted, +} from 'core_form/events'; +import {getFilePicker} from 'editor_tiny/options'; + +// This image uploader is based on advice given at: +// https://www.tiny.cloud/docs/tinymce/6/upload-images/ +export default (activeEditor, blobInfo, progress) => new Promise((resolve, reject) => { + notifyUploadStarted(activeEditor.targetElm.id); + debugger; // eslint-disable-line + + const xhr = new XMLHttpRequest(); + + // Add the progress handler. + xhr.upload.addEventListener('progress', (e) => { + progress(e.loaded / e.total * 100); + }); + + xhr.addEventListener('load', () => { + if (xhr.status === 403) { + reject({ + message: `HTTP error: ${xhr.status}`, + remove: true, + }); + return; + } + + if (xhr.status < 200 || xhr.status >= 300) { + reject(`HTTP Error: ${xhr.status}`); + return; + } + + const response = JSON.parse(xhr.responseText); + + if (!response) { + reject(`Invalid JSON: ${xhr.responseText}`); + return; + } + + notifyUploadCompleted(activeEditor.targetElm.id); + + let location; + if (response.url) { + location = response.url; + } else if (response.event && response.event === 'fileexists' && response.newfile) { + // A file with this name is already in use here - rename to avoid conflict. + // Chances are, it's a different image (stored in a different folder on the user's computer). + // If the user wants to reuse an existing image, they can copy/paste it within the editor. + location = response.newfile.url; + } + + if (location && typeof location === 'string') { + resolve(location); + return; + } + + reject(`Unable to handle file result: ${xhr.responseText}`); + }); + + xhr.addEventListener('error', () => { + reject(`Image upload failed due to an XHR transport error. Code: ${xhr.status}`); + }); + + const formData = new FormData(); + const options = getFilePicker(activeEditor, 'image'); + + formData.append('repo_upload_file', blobInfo.blob()); + formData.append('itemid', options.itemid); + Object.values(options.repositories).some((repository) => { + if (repository.type === 'upload') { + formData.append('repo_id', repository.id); + return true; + } + return false; + }); + + formData.append('env', options.env); + formData.append('sesskey', M.cfg.sesskey); + formData.append('client_id', options.client_id); + formData.append('savepath', options.savepath ?? '/'); + formData.append('ctx_id', options.context.id); + + xhr.open('POST', `${M.cfg.wwwroot}/repository/repository_ajax.php?action=upload`, true); + xhr.send(formData); +}); diff --git a/lib/editor/tiny/plugins/media/classes/plugininfo.php b/lib/editor/tiny/plugins/media/classes/plugininfo.php new file mode 100644 index 0000000000000..73fad15184561 --- /dev/null +++ b/lib/editor/tiny/plugins/media/classes/plugininfo.php @@ -0,0 +1,57 @@ +. + +namespace tiny_media; + +use context; +use editor_tiny\editor; +use editor_tiny\plugin; +use editor_tiny\plugin_with_buttons; +use editor_tiny\plugin_with_configuration; +use editor_tiny\plugin_with_menuitems; + +/** + * Tiny media plugin. + * + * @package tiny_media + * @copyright 2022 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class plugininfo extends plugin implements plugin_with_buttons, plugin_with_menuitems, plugin_with_configuration { + + public static function get_available_buttons(): array { + return [ + 'tiny_media/tiny_media_image', + ]; + } + + public static function get_available_menuitems(): array { + return [ + 'tiny_media/tiny_media_image', + ]; + } + + public static function get_plugin_configuration_for_context(context $context, array $options, array $fpoptions, + ?editor $editor = null): array { + $permissions = [ + 'upload' => true, + ]; + return [ + 'permissions' => $permissions, + 'storeinrepo' => true, + ]; + } +} diff --git a/lib/editor/tiny/plugins/media/classes/privacy/provider.php b/lib/editor/tiny/plugins/media/classes/privacy/provider.php new file mode 100644 index 0000000000000..fd4727d111c0f --- /dev/null +++ b/lib/editor/tiny/plugins/media/classes/privacy/provider.php @@ -0,0 +1,31 @@ +. + +/** + * Privacy Subsystem implementation for the media plugin for TinyMCE. + * + * @package tiny_media + * @copyright 2022 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tiny_media\privacy; + +class provider implements \core_privacy\local\metadata\null_provider { + public static function get_reason(): string { + return 'privacy:metadata'; + } +} diff --git a/lib/editor/tiny/plugins/media/lang/en/tiny_media.php b/lib/editor/tiny/plugins/media/lang/en/tiny_media.php new file mode 100644 index 0000000000000..dcdcc4f925599 --- /dev/null +++ b/lib/editor/tiny/plugins/media/lang/en/tiny_media.php @@ -0,0 +1,47 @@ +. + +/** + * Strings for component 'tiny_media', language 'en'. + * + * @package tiny_media + * @copyright 2022 Andrew Lyons + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['alignment'] = 'Alignment'; +$string['alignment_bottom'] = 'Bottom'; +$string['alignment_left'] = 'Left'; +$string['alignment_middle'] = 'Middle'; +$string['alignment_right'] = 'Right'; +$string['alignment_top'] = 'Top'; +$string['browserepositories'] = 'Browse repositories...'; +$string['constrain'] = 'Auto size'; +$string['enteralt'] = 'Describe this image for someone who cannot see it'; +$string['enterurl'] = 'Enter URL'; +$string['height'] = 'Height'; +$string['helplinktext'] = 'Moodle Media Helper'; +$string['imagebuttontitle'] = 'Moodle Image'; +$string['imageurlrequired'] = 'An image must have a URL.'; +$string['imageproperties'] = 'Image properties'; +$string['presentation'] = 'This image is decorative only'; +$string['presentationoraltrequired'] = 'An image must have a description, unless it is marked as decorative only.'; +$string['privacy:metadata'] = 'The media plugin for TinyMCE does not store any personal data.'; +$string['pluginname'] = 'Tiny Media plugin for Moodle'; +$string['privacy:metadata'] = 'The media plugin for TinyMCE does not store any personal data.'; +$string['saveimage'] = 'Save image'; +$string['size'] = 'Size'; +$string['width'] = 'Width'; diff --git a/lib/editor/tiny/plugins/media/templates/image.mustache b/lib/editor/tiny/plugins/media/templates/image.mustache new file mode 100644 index 0000000000000..3b7d3af67f659 --- /dev/null +++ b/lib/editor/tiny/plugins/media/templates/image.mustache @@ -0,0 +1,34 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template tiny_media/image + + Image template. + + Example context (json): + { + + } +}} +{{alt}} diff --git a/lib/editor/tiny/plugins/media/templates/insert_image.mustache b/lib/editor/tiny/plugins/media/templates/insert_image.mustache new file mode 100644 index 0000000000000..49be585d3b312 --- /dev/null +++ b/lib/editor/tiny/plugins/media/templates/insert_image.mustache @@ -0,0 +1,104 @@ +{{! + 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 . +}} +{{! + @template tiny_media/insert_image + + Insert image template. + + Example context (json): + { + + } +}} +
+ + {{#showfilepicker}} +
+ +
+ + + + +
+
+ {{/showfilepicker}} + {{^showfilepicker}} +
+ + +
+ {{/showfilepicker}} + +
+ + + +
+ 0 + / 125 +
+ +
+ + +
+
+
+ +
+ + + x + + + +
+ + +
+
+
+
+ + +
+ +
+
+
+ +
+ +
+
diff --git a/lib/editor/tiny/plugins/media/version.php b/lib/editor/tiny/plugins/media/version.php new file mode 100644 index 0000000000000..e2f4250a07ea2 --- /dev/null +++ b/lib/editor/tiny/plugins/media/version.php @@ -0,0 +1,29 @@ +. + +/** + * Tiny media plugin version details. + * + * @package tiny_media + * @copyright 2022 Huong Nguyen + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->version = 2022062700; +$plugin->requires = 2021052500; +$plugin->component = 'tiny_media';