diff --git a/lang/en/moodle.php b/lang/en/moodle.php
index a8e2c53490c..d7d0d5a19a4 100644
--- a/lang/en/moodle.php
+++ b/lang/en/moodle.php
@@ -1297,6 +1297,7 @@ $string['maximumgradex'] = 'Maximum grade: {$a}';
$string['maximumshort'] = 'Max';
$string['maximumupload'] = 'Maximum upload size';
$string['maximumupload_help'] = 'The maximum file size allowed for student uploads to the course. Additionally, you can further restrict the maximum upload size for each activity.';
+$string['maxlengthreached'] = 'Maximum character limit of {$a} has been reached';
$string['maxnumberweeks'] = 'Maximum number of sections';
$string['maxnumberweeks_desc'] = 'The maximum value in the number of sections drop-down menu (applies to certain course formats only).';
$string['maxnumcoursesincombo'] = 'Browse {$a->numberofcourses} courses.';
diff --git a/lib/editor/tiny/plugins/media/amd/build/imagedetails.min.js b/lib/editor/tiny/plugins/media/amd/build/imagedetails.min.js
index b5397f96d39..c35855675fc 100644
--- a/lib/editor/tiny/plugins/media/amd/build/imagedetails.min.js
+++ b/lib/editor/tiny/plugins/media/amd/build/imagedetails.min.js
@@ -1,3 +1,3 @@
-define("tiny_media/imagedetails",["exports","core/config","core/modal_events","core/notification","core/pending","./selectors","core/templates","core/str","tiny_media/imageinsert","./mediabase","./helpers"],(function(_exports,_config,_modal_events,_notification,_pending,_selectors,_templates,_str,_imageinsert,_mediabase,_helpers){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.ImageDetails=void 0,_config=_interopRequireDefault(_config),_modal_events=_interopRequireDefault(_modal_events),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending),_selectors=_interopRequireDefault(_selectors),_templates=_interopRequireDefault(_templates);class ImageDetails extends _mediabase.MediaBase{constructor(root,editor,currentModal,canShowFilePicker,canShowDropZone,currentUrl,_image){super(),_defineProperty(this,"DEFAULTS",{WIDTH:160,HEIGHT:160}),_defineProperty(this,"selectorType",_selectors.default.IMAGE.type),_defineProperty(this,"mediaDimensions",null),_defineProperty(this,"init",(function(){this.currentModal.setTitle((0,_str.getString)("imagedetails","tiny_media")),this.imageTypeChecked(),this.presentationChanged(),this.storeImageDimensions(this.image),this.setImageDimensions(),this.registerEventListeners()})),_defineProperty(this,"loadInsertImage",(async function(){const templateContext={elementid:this.editor.id,showfilepicker:this.canShowFilePicker,showdropzone:this.canShowDropZone,bodyTemplate:_selectors.default.IMAGE.template.body.insertImageBody,footerTemplate:_selectors.default.IMAGE.template.footer.insertImageFooter,selector:_selectors.default.IMAGE.type};Promise.all([(0,_helpers.body)(templateContext,this.root),(0,_helpers.footer)(templateContext,this.root)]).then((()=>{new _imageinsert.ImageInsert(this.root,this.editor,this.currentModal,this.canShowFilePicker,this.canShowDropZone).init()})).catch((error=>{window.console.log(error)}))})),_defineProperty(this,"setImageDimensions",(()=>{const imagePreviewBox=this.root.querySelector(_selectors.default.IMAGE.elements.previewBox),image=this.root.querySelector(_selectors.default.IMAGE.elements.preview),widthField=this.root.querySelector(_selectors.default.IMAGE.elements.width),heightField=this.root.querySelector(_selectors.default.IMAGE.elements.height),updateImageDimensions=()=>{const boxWidth=imagePreviewBox.clientWidth,boxHeight=imagePreviewBox.clientHeight,dimensions=this.fitSquareIntoBox(widthField.value,heightField.value,boxWidth,boxHeight);image.style.width="".concat(dimensions.width,"px"),image.style.height="".concat(dimensions.height,"px")};0===imagePreviewBox.clientWidth?this.currentModal.getRoot().on(_modal_events.default.shown,(()=>{updateImageDimensions()})):updateImageDimensions()})),_defineProperty(this,"fitSquareIntoBox",((squareWidth,squareHeight,boxWidth,boxHeight)=>{if(squareWidth(""===element.value&&(element.value=this.mediaDimensions.width),element.value))(this.root.querySelector(_selectors.default.IMAGE.elements.width)),currentHeight=(element=>(""===element.value&&(element.value=this.mediaDimensions.height),element.value))(this.root.querySelector(_selectors.default.IMAGE.elements.height)),preview=this.root.querySelector(_selectors.default.IMAGE.elements.preview);preview.setAttribute("src",image.src),preview.style.display="";const constrain=this.root.querySelector(_selectors.default.IMAGE.elements.constrain);if((0,_helpers.isPercentageValue)(currentWidth)&&(0,_helpers.isPercentageValue)(currentHeight))constrain.checked=currentWidth===currentHeight;else if(0===image.width||0===image.height)constrain.disabled="disabled";else{const widthRatio=Math.round(100*parseInt(currentWidth,10)/image.width),heightRatio=Math.round(100*parseInt(currentHeight,10)/image.height);constrain.checked=widthRatio===heightRatio}((currentWidth,currentHeight)=>{this.mediaDimensions.width===currentWidth&&this.mediaDimensions.height===currentHeight?(this.currentWidth=this.mediaDimensions.width,this.currentHeight=this.mediaDimensions.height,this.sizeChecked("original")):(this.currentWidth=currentWidth,this.currentHeight=currentHeight,this.sizeChecked("custom"))})(Number(currentWidth),Number(currentHeight))}presentationChanged(){const presentation=this.root.querySelector(_selectors.default.IMAGE.elements.presentation);this.root.querySelector(_selectors.default.IMAGE.elements.alt).disabled=presentation.checked,this.handleKeyupCharacterCount()}imageTypeChecked(){const isExternalUrl=!1===new RegExp("".concat(_config.default.wwwroot)).test(this.currentUrl);if((0,_helpers.hideElements)(_selectors.default.IMAGE.elements.url,this.root),isExternalUrl)this.setFilenameLabel(decodeURI(this.currentUrl));else{const filename=this.currentUrl.split("/").pop().split("?")[0];this.setFilenameLabel(decodeURI(filename))}}setFilenameLabel(label){const urlLabelEle=this.root.querySelector(_selectors.default.IMAGE.elements.fileNameLabel);urlLabelEle&&(urlLabelEle.innerHTML=label,urlLabelEle.setAttribute("title",label))}toggleAriaInvalid(selectors,predicate){selectors.forEach((selector=>{this.root.querySelectorAll(selector).forEach((element=>element.setAttribute("aria-invalid",predicate)))}))}hasErrorUrlField(){const urlError=""===this.currentUrl;return urlError?(0,_helpers.showElements)(_selectors.default.IMAGE.elements.urlWarning,this.root):(0,_helpers.hideElements)(_selectors.default.IMAGE.elements.urlWarning,this.root),this.toggleAriaInvalid([_selectors.default.IMAGE.elements.url],urlError),urlError}hasErrorAltField(){const alt=this.root.querySelector(_selectors.default.IMAGE.elements.alt).value,presentation=this.root.querySelector(_selectors.default.IMAGE.elements.presentation).checked,imageAltError=""===alt&&!presentation;return imageAltError?(0,_helpers.showElements)(_selectors.default.IMAGE.elements.altWarning,this.root):(0,_helpers.hideElements)(_selectors.default.IMAGE.elements.altWarning,this.root),this.toggleAriaInvalid([_selectors.default.IMAGE.elements.alt,_selectors.default.IMAGE.elements.presentation],imageAltError),imageAltError}updateWarning(){const urlError=this.hasErrorUrlField(),imageAltError=this.hasErrorAltField();return urlError||imageAltError}getImageContext(){if(this.updateWarning())return null;const classList=[],constrain=this.root.querySelector(_selectors.default.IMAGE.elements.constrain).checked,sizeOriginal=this.root.querySelector(_selectors.default.IMAGE.elements.sizeOriginal).checked;return constrain||sizeOriginal?classList.push(_selectors.default.IMAGE.styles.responsive):classList.pop(_selectors.default.IMAGE.styles.responsive),{url:this.currentUrl,alt:this.root.querySelector(_selectors.default.IMAGE.elements.alt).value,width:this.root.querySelector(_selectors.default.IMAGE.elements.width).value,height:this.root.querySelector(_selectors.default.IMAGE.elements.height).value,presentation:this.root.querySelector(_selectors.default.IMAGE.elements.presentation).checked,customStyle:this.root.querySelector(_selectors.default.IMAGE.elements.customStyle).value,classlist:classList.join(" ")}}setImage(){const pendingPromise=new _pending.default("tiny_media:setImage");if(""===this.currentUrl)return;if(this.updateWarning())return void pendingPromise.resolve();const width=this.root.querySelector(_selectors.default.IMAGE.elements.width).value;if(!(0,_helpers.isPercentageValue)(width)&&isNaN(parseInt(width,10)))return this.root.querySelector(_selectors.default.IMAGE.elements.width).focus(),void pendingPromise.resolve();const height=this.root.querySelector(_selectors.default.IMAGE.elements.height).value;if(!(0,_helpers.isPercentageValue)(height)&&isNaN(parseInt(height,10)))return this.root.querySelector(_selectors.default.IMAGE.elements.height).focus(),void pendingPromise.resolve();_templates.default.render("tiny_media/image",this.getImageContext()).then((html=>(this.editor.insertContent(html),this.currentModal.destroy(),pendingPromise.resolve(),html))).catch((error=>{window.console.log(error)}))}deleteImage(){_notification.default.deleteCancelPromise((0,_str.getString)("deleteimage","tiny_media"),(0,_str.getString)("deleteimagewarning","tiny_media")).then((()=>{(0,_helpers.hideElements)(_selectors.default.IMAGE.elements.altWarning,this.root),this.loadInsertImage()})).catch((error=>{window.console.log(error)}))}registerEventListeners(){this.root.querySelector(_selectors.default.IMAGE.actions.submit).addEventListener("click",(e=>{e.preventDefault(),this.setImage()}));const deleteImageEle=this.root.querySelector(_selectors.default.IMAGE.actions.deleteImage);deleteImageEle.addEventListener("click",(()=>{this.deleteImage()})),deleteImageEle.addEventListener("keydown",(e=>{"Enter"===e.key&&this.deleteImage()})),this.root.addEventListener("change",(e=>{e.target.closest(_selectors.default.IMAGE.elements.presentation)&&this.presentationChanged();e.target.closest(_selectors.default.IMAGE.elements.constrain)&&this.autoAdjustSize();e.target.closest(_selectors.default.IMAGE.elements.sizeOriginal)&&this.sizeChecked("original");e.target.closest(_selectors.default.IMAGE.elements.sizeCustom)&&this.sizeChecked("custom")})),this.root.addEventListener("blur",(e=>{if(e.target.nodeType===Node.ELEMENT_NODE){e.target.closest(_selectors.default.IMAGE.elements.presentation)&&this.presentationChanged()}}),!0),this.root.addEventListener("keyup",(e=>{e.target.closest(_selectors.default.IMAGE.elements.alt)&&this.handleKeyupCharacterCount()})),this.root.addEventListener("input",(e=>{const widthEle=e.target.closest(_selectors.default.IMAGE.elements.width);widthEle&&(widthEle.value=""===widthEle.value?0:Number(widthEle.value),this.autoAdjustSize());const heightEle=e.target.closest(_selectors.default.IMAGE.elements.height);heightEle&&(heightEle.value=""===heightEle.value?0:Number(heightEle.value),this.autoAdjustSize(!0))}))}handleKeyupCharacterCount(){const alt=this.root.querySelector(_selectors.default.IMAGE.elements.alt).value;this.root.querySelector("#currentcount").innerHTML=alt.length}}_exports.ImageDetails=ImageDetails}));
+define("tiny_media/imagedetails",["exports","core/config","core/modal_events","core/notification","core/pending","./selectors","core/templates","core/str","tiny_media/imageinsert","./mediabase","./helpers"],(function(_exports,_config,_modal_events,_notification,_pending,_selectors,_templates,_str,_imageinsert,_mediabase,_helpers){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.ImageDetails=void 0,_config=_interopRequireDefault(_config),_modal_events=_interopRequireDefault(_modal_events),_notification=_interopRequireDefault(_notification),_pending=_interopRequireDefault(_pending),_selectors=_interopRequireDefault(_selectors),_templates=_interopRequireDefault(_templates);class ImageDetails extends _mediabase.MediaBase{constructor(root,editor,currentModal,canShowFilePicker,canShowDropZone,currentUrl,_image){super(),_defineProperty(this,"DEFAULTS",{WIDTH:160,HEIGHT:160}),_defineProperty(this,"selectorType",_selectors.default.IMAGE.type),_defineProperty(this,"mediaDimensions",null),_defineProperty(this,"init",(function(){this.currentModal.setTitle((0,_str.getString)("imagedetails","tiny_media")),this.imageTypeChecked(),this.presentationChanged(),this.storeImageDimensions(this.image),this.setImageDimensions(),this.registerEventListeners()})),_defineProperty(this,"loadInsertImage",(async function(){const templateContext={elementid:this.editor.id,showfilepicker:this.canShowFilePicker,showdropzone:this.canShowDropZone,bodyTemplate:_selectors.default.IMAGE.template.body.insertImageBody,footerTemplate:_selectors.default.IMAGE.template.footer.insertImageFooter,selector:_selectors.default.IMAGE.type};Promise.all([(0,_helpers.body)(templateContext,this.root),(0,_helpers.footer)(templateContext,this.root)]).then((()=>{new _imageinsert.ImageInsert(this.root,this.editor,this.currentModal,this.canShowFilePicker,this.canShowDropZone).init()})).catch((error=>{window.console.log(error)}))})),_defineProperty(this,"setImageDimensions",(()=>{const imagePreviewBox=this.root.querySelector(_selectors.default.IMAGE.elements.previewBox),image=this.root.querySelector(_selectors.default.IMAGE.elements.preview),widthField=this.root.querySelector(_selectors.default.IMAGE.elements.width),heightField=this.root.querySelector(_selectors.default.IMAGE.elements.height),updateImageDimensions=()=>{const boxWidth=imagePreviewBox.clientWidth,boxHeight=imagePreviewBox.clientHeight,dimensions=this.fitSquareIntoBox(widthField.value,heightField.value,boxWidth,boxHeight);image.style.width="".concat(dimensions.width,"px"),image.style.height="".concat(dimensions.height,"px")};0===imagePreviewBox.clientWidth?this.currentModal.getRoot().on(_modal_events.default.shown,(()=>{updateImageDimensions()})):updateImageDimensions()})),_defineProperty(this,"fitSquareIntoBox",((squareWidth,squareHeight,boxWidth,boxHeight)=>{if(squareWidth(""===element.value&&(element.value=this.mediaDimensions.width),element.value))(this.root.querySelector(_selectors.default.IMAGE.elements.width)),currentHeight=(element=>(""===element.value&&(element.value=this.mediaDimensions.height),element.value))(this.root.querySelector(_selectors.default.IMAGE.elements.height)),preview=this.root.querySelector(_selectors.default.IMAGE.elements.preview);preview.setAttribute("src",image.src),preview.style.display="";const constrain=this.root.querySelector(_selectors.default.IMAGE.elements.constrain);if((0,_helpers.isPercentageValue)(currentWidth)&&(0,_helpers.isPercentageValue)(currentHeight))constrain.checked=currentWidth===currentHeight;else if(0===image.width||0===image.height)constrain.disabled="disabled";else{const widthRatio=Math.round(100*parseInt(currentWidth,10)/image.width),heightRatio=Math.round(100*parseInt(currentHeight,10)/image.height);constrain.checked=widthRatio===heightRatio}((currentWidth,currentHeight)=>{this.mediaDimensions.width===currentWidth&&this.mediaDimensions.height===currentHeight?(this.currentWidth=this.mediaDimensions.width,this.currentHeight=this.mediaDimensions.height,this.sizeChecked("original")):(this.currentWidth=currentWidth,this.currentHeight=currentHeight,this.sizeChecked("custom"))})(Number(currentWidth),Number(currentHeight))}async presentationChanged(){const presentation=this.root.querySelector(_selectors.default.IMAGE.elements.presentation);this.root.querySelector(_selectors.default.IMAGE.elements.alt).disabled=presentation.checked,await this.handleKeyupCharacterCount()}imageTypeChecked(){const isExternalUrl=!1===new RegExp("".concat(_config.default.wwwroot)).test(this.currentUrl);if((0,_helpers.hideElements)(_selectors.default.IMAGE.elements.url,this.root),isExternalUrl)this.setFilenameLabel(decodeURI(this.currentUrl));else{const filename=this.currentUrl.split("/").pop().split("?")[0];this.setFilenameLabel(decodeURI(filename))}}setFilenameLabel(label){const urlLabelEle=this.root.querySelector(_selectors.default.IMAGE.elements.fileNameLabel);urlLabelEle&&(urlLabelEle.innerHTML=label,urlLabelEle.setAttribute("title",label))}toggleAriaInvalid(selectors,predicate){selectors.forEach((selector=>{this.root.querySelectorAll(selector).forEach((element=>element.setAttribute("aria-invalid",predicate)))}))}hasErrorUrlField(){const urlError=""===this.currentUrl;return urlError?(0,_helpers.showElements)(_selectors.default.IMAGE.elements.urlWarning,this.root):(0,_helpers.hideElements)(_selectors.default.IMAGE.elements.urlWarning,this.root),this.toggleAriaInvalid([_selectors.default.IMAGE.elements.url],urlError),urlError}hasErrorAltField(){const alt=this.root.querySelector(_selectors.default.IMAGE.elements.alt).value,presentation=this.root.querySelector(_selectors.default.IMAGE.elements.presentation).checked,imageAltError=""===alt&&!presentation;return imageAltError?(0,_helpers.showElements)(_selectors.default.IMAGE.elements.altWarning,this.root):(0,_helpers.hideElements)(_selectors.default.IMAGE.elements.altWarning,this.root),this.toggleAriaInvalid([_selectors.default.IMAGE.elements.alt,_selectors.default.IMAGE.elements.presentation],imageAltError),imageAltError}updateWarning(){const urlError=this.hasErrorUrlField(),imageAltError=this.hasErrorAltField();return urlError||imageAltError}getImageContext(){if(this.updateWarning())return null;const classList=[],constrain=this.root.querySelector(_selectors.default.IMAGE.elements.constrain).checked,sizeOriginal=this.root.querySelector(_selectors.default.IMAGE.elements.sizeOriginal).checked;return constrain||sizeOriginal?classList.push(_selectors.default.IMAGE.styles.responsive):classList.pop(_selectors.default.IMAGE.styles.responsive),{url:this.currentUrl,alt:this.root.querySelector(_selectors.default.IMAGE.elements.alt).value,width:this.root.querySelector(_selectors.default.IMAGE.elements.width).value,height:this.root.querySelector(_selectors.default.IMAGE.elements.height).value,presentation:this.root.querySelector(_selectors.default.IMAGE.elements.presentation).checked,customStyle:this.root.querySelector(_selectors.default.IMAGE.elements.customStyle).value,classlist:classList.join(" ")}}setImage(){const pendingPromise=new _pending.default("tiny_media:setImage");if(""===this.currentUrl)return;if(this.updateWarning())return void pendingPromise.resolve();const width=this.root.querySelector(_selectors.default.IMAGE.elements.width).value;if(!(0,_helpers.isPercentageValue)(width)&&isNaN(parseInt(width,10)))return this.root.querySelector(_selectors.default.IMAGE.elements.width).focus(),void pendingPromise.resolve();const height=this.root.querySelector(_selectors.default.IMAGE.elements.height).value;if(!(0,_helpers.isPercentageValue)(height)&&isNaN(parseInt(height,10)))return this.root.querySelector(_selectors.default.IMAGE.elements.height).focus(),void pendingPromise.resolve();_templates.default.render("tiny_media/image",this.getImageContext()).then((html=>(this.editor.insertContent(html),this.currentModal.destroy(),pendingPromise.resolve(),html))).catch((error=>{window.console.log(error)}))}deleteImage(){_notification.default.deleteCancelPromise((0,_str.getString)("deleteimage","tiny_media"),(0,_str.getString)("deleteimagewarning","tiny_media")).then((()=>{(0,_helpers.hideElements)(_selectors.default.IMAGE.elements.altWarning,this.root),this.loadInsertImage()})).catch((error=>{window.console.log(error)}))}registerEventListeners(){this.root.querySelector(_selectors.default.IMAGE.actions.submit).addEventListener("click",(e=>{e.preventDefault(),this.setImage()}));const deleteImageEle=this.root.querySelector(_selectors.default.IMAGE.actions.deleteImage);deleteImageEle.addEventListener("click",(()=>{this.deleteImage()})),deleteImageEle.addEventListener("keydown",(e=>{"Enter"===e.key&&this.deleteImage()})),this.root.addEventListener("change",(async e=>{e.target.closest(_selectors.default.IMAGE.elements.presentation)&&await this.presentationChanged();e.target.closest(_selectors.default.IMAGE.elements.constrain)&&this.autoAdjustSize();e.target.closest(_selectors.default.IMAGE.elements.sizeOriginal)&&this.sizeChecked("original");e.target.closest(_selectors.default.IMAGE.elements.sizeCustom)&&this.sizeChecked("custom")})),this.root.addEventListener("blur",(async e=>{if(e.target.nodeType===Node.ELEMENT_NODE){e.target.closest(_selectors.default.IMAGE.elements.presentation)&&await this.presentationChanged()}}),!0),this.root.addEventListener("keyup",(async e=>{e.target.closest(_selectors.default.IMAGE.elements.alt)&&await this.handleKeyupCharacterCount()})),this.root.addEventListener("input",(e=>{const widthEle=e.target.closest(_selectors.default.IMAGE.elements.width);widthEle&&(widthEle.value=""===widthEle.value?0:Number(widthEle.value),this.autoAdjustSize());const heightEle=e.target.closest(_selectors.default.IMAGE.elements.height);heightEle&&(heightEle.value=""===heightEle.value?0:Number(heightEle.value),this.autoAdjustSize(!0))}))}async handleKeyupCharacterCount(){const altField=this.root.querySelector(_selectors.default.IMAGE.elements.alt),alt=altField.value;this.root.querySelector("#currentcount").innerHTML=alt.length;const maxLength=altField.getAttribute("maxlength"),maxLengthFeedback=document.getElementById("maxlength_feedback");if(alt.length>=maxLength){maxLengthFeedback.textContent=await(0,_str.getString)("maxlengthreached","core",maxLength);const suffix=this.toggleMaxlengthFeedbackSuffix?"":".";maxLengthFeedback.textContent+=suffix,this.toggleMaxlengthFeedbackSuffix=!this.toggleMaxlengthFeedbackSuffix,setTimeout((()=>{maxLengthFeedback.textContent=""}),4e3)}}}_exports.ImageDetails=ImageDetails}));
//# sourceMappingURL=imagedetails.min.js.map
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/media/amd/build/imagedetails.min.js.map b/lib/editor/tiny/plugins/media/amd/build/imagedetails.min.js.map
index 92e6f5268d7..c47197b2b23 100644
--- a/lib/editor/tiny/plugins/media/amd/build/imagedetails.min.js.map
+++ b/lib/editor/tiny/plugins/media/amd/build/imagedetails.min.js.map
@@ -1 +1 @@
-{"version":3,"file":"imagedetails.min.js","sources":["../src/imagedetails.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 details class for Moodle.\n *\n * @module tiny_media/imagedetails\n * @copyright 2024 Meirza \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Config from 'core/config';\nimport ModalEvents from 'core/modal_events';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport Selectors from './selectors';\nimport Templates from 'core/templates';\nimport {getString} from 'core/str';\nimport {ImageInsert} from 'tiny_media/imageinsert';\nimport {MediaBase} from './mediabase';\nimport {\n body,\n footer,\n hideElements,\n showElements,\n isPercentageValue,\n} from './helpers';\n\nexport class ImageDetails extends MediaBase {\n DEFAULTS = {\n WIDTH: 160,\n HEIGHT: 160,\n };\n\n selectorType = Selectors.IMAGE.type;\n\n mediaDimensions = null;\n\n constructor(\n root,\n editor,\n currentModal,\n canShowFilePicker,\n canShowDropZone,\n currentUrl,\n image,\n ) {\n super();\n this.root = root;\n this.editor = editor;\n this.currentModal = currentModal;\n this.canShowFilePicker = canShowFilePicker;\n this.canShowDropZone = canShowDropZone;\n this.currentUrl = currentUrl;\n this.image = image;\n }\n\n init = function() {\n this.currentModal.setTitle(getString('imagedetails', 'tiny_media'));\n this.imageTypeChecked();\n this.presentationChanged();\n this.storeImageDimensions(this.image);\n this.setImageDimensions();\n this.registerEventListeners();\n };\n\n /**\n * Loads and displays a preview image based on the provided URL, and handles image loading events.\n */\n loadInsertImage = async function() {\n const templateContext = {\n elementid: this.editor.id,\n showfilepicker: this.canShowFilePicker,\n showdropzone: this.canShowDropZone,\n bodyTemplate: Selectors.IMAGE.template.body.insertImageBody,\n footerTemplate: Selectors.IMAGE.template.footer.insertImageFooter,\n selector: Selectors.IMAGE.type,\n };\n\n Promise.all([body(templateContext, this.root), footer(templateContext, this.root)])\n .then(() => {\n const imageinsert = new ImageInsert(\n this.root,\n this.editor,\n this.currentModal,\n this.canShowFilePicker,\n this.canShowDropZone,\n );\n imageinsert.init();\n return;\n })\n .catch(error => {\n window.console.log(error);\n });\n };\n\n storeImageDimensions(image) {\n // Store dimensions of the raw image, falling back to defaults for images without dimensions (e.g. SVG).\n this.mediaDimensions = {\n width: image.width || this.DEFAULTS.WIDTH,\n height: image.height || this.DEFAULTS.HEIGHT,\n };\n\n const getCurrentWidth = (element) => {\n if (element.value === '') {\n element.value = this.mediaDimensions.width;\n }\n return element.value;\n };\n\n const getCurrentHeight = (element) => {\n if (element.value === '') {\n element.value = this.mediaDimensions.height;\n }\n return element.value;\n };\n\n const widthInput = this.root.querySelector(Selectors.IMAGE.elements.width);\n const currentWidth = getCurrentWidth(widthInput);\n\n const heightInput = this.root.querySelector(Selectors.IMAGE.elements.height);\n const currentHeight = getCurrentHeight(heightInput);\n\n const preview = this.root.querySelector(Selectors.IMAGE.elements.preview);\n preview.setAttribute('src', image.src);\n preview.style.display = '';\n\n // Ensure the checkbox always in unchecked status when an image loads at first.\n const constrain = this.root.querySelector(Selectors.IMAGE.elements.constrain);\n if (isPercentageValue(currentWidth) && isPercentageValue(currentHeight)) {\n constrain.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 constrain.disabled = 'disabled';\n } else {\n // This is the same as comparing to 3 decimal places.\n const widthRatio = Math.round(100 * parseInt(currentWidth, 10) / image.width);\n const heightRatio = Math.round(100 * parseInt(currentHeight, 10) / image.height);\n constrain.checked = widthRatio === heightRatio;\n }\n\n /**\n * Sets the selected size option based on current width and height values.\n *\n * @param {number} currentWidth - The current width value.\n * @param {number} currentHeight - The current height value.\n */\n const setSelectedSize = (currentWidth, currentHeight) => {\n if (this.mediaDimensions.width === currentWidth &&\n this.mediaDimensions.height === currentHeight\n ) {\n this.currentWidth = this.mediaDimensions.width;\n this.currentHeight = this.mediaDimensions.height;\n this.sizeChecked('original');\n } else {\n this.currentWidth = currentWidth;\n this.currentHeight = currentHeight;\n this.sizeChecked('custom');\n }\n };\n\n setSelectedSize(Number(currentWidth), Number(currentHeight));\n }\n\n /**\n * Sets the dimensions of the image preview element based on user input and constraints.\n */\n setImageDimensions = () => {\n const imagePreviewBox = this.root.querySelector(Selectors.IMAGE.elements.previewBox);\n const image = this.root.querySelector(Selectors.IMAGE.elements.preview);\n const widthField = this.root.querySelector(Selectors.IMAGE.elements.width);\n const heightField = this.root.querySelector(Selectors.IMAGE.elements.height);\n\n const updateImageDimensions = () => {\n // Get the latest dimensions of the preview box for responsiveness.\n const boxWidth = imagePreviewBox.clientWidth;\n const boxHeight = imagePreviewBox.clientHeight;\n // Get the new width and height for the image.\n const dimensions = this.fitSquareIntoBox(widthField.value, heightField.value, boxWidth, boxHeight);\n image.style.width = `${dimensions.width}px`;\n image.style.height = `${dimensions.height}px`;\n };\n // If the client size is zero, then get the new dimensions once the modal is shown.\n if (imagePreviewBox.clientWidth === 0) {\n // Call the shown event.\n this.currentModal.getRoot().on(ModalEvents.shown, () => {\n updateImageDimensions();\n });\n } else {\n updateImageDimensions();\n }\n };\n\n /**\n * Handles changes in the image presentation checkbox and enables/disables the image alt text input accordingly.\n */\n presentationChanged() {\n const presentation = this.root.querySelector(Selectors.IMAGE.elements.presentation);\n const alt = this.root.querySelector(Selectors.IMAGE.elements.alt);\n alt.disabled = presentation.checked;\n\n // Counting the image description characters.\n this.handleKeyupCharacterCount();\n }\n\n /**\n * This function checks whether an image URL is local (within the same website's domain) or external (from an external source).\n * Depending on the result, it dynamically updates the visibility and content of HTML elements in a user interface.\n * If the image is local then we only show it's filename.\n * If the image is external then it will show full URL and it can be updated.\n */\n imageTypeChecked() {\n const regex = new RegExp(`${Config.wwwroot}`);\n\n // True if the URL is from external, otherwise false.\n const isExternalUrl = regex.test(this.currentUrl) === false;\n\n // Hide the URL input.\n hideElements(Selectors.IMAGE.elements.url, this.root);\n\n if (!isExternalUrl) {\n // Split the URL by '/' to get an array of segments.\n const segments = this.currentUrl.split('/');\n // Get the last segment, which should be the filename.\n const filename = segments.pop().split('?')[0];\n // Show the file name.\n this.setFilenameLabel(decodeURI(filename));\n } else {\n\n this.setFilenameLabel(decodeURI(this.currentUrl));\n }\n }\n\n /**\n * Set the string for the URL label element.\n *\n * @param {string} label - The label text to set.\n */\n setFilenameLabel(label) {\n const urlLabelEle = this.root.querySelector(Selectors.IMAGE.elements.fileNameLabel);\n if (urlLabelEle) {\n urlLabelEle.innerHTML = label;\n urlLabelEle.setAttribute(\"title\", label);\n }\n }\n\n toggleAriaInvalid(selectors, predicate) {\n selectors.forEach((selector) => {\n const elements = this.root.querySelectorAll(selector);\n elements.forEach((element) => element.setAttribute('aria-invalid', predicate));\n });\n }\n\n hasErrorUrlField() {\n const urlError = this.currentUrl === '';\n if (urlError) {\n showElements(Selectors.IMAGE.elements.urlWarning, this.root);\n } else {\n hideElements(Selectors.IMAGE.elements.urlWarning, this.root);\n }\n this.toggleAriaInvalid([Selectors.IMAGE.elements.url], urlError);\n\n return urlError;\n }\n\n hasErrorAltField() {\n const alt = this.root.querySelector(Selectors.IMAGE.elements.alt).value;\n const presentation = this.root.querySelector(Selectors.IMAGE.elements.presentation).checked;\n const imageAltError = alt === '' && !presentation;\n if (imageAltError) {\n showElements(Selectors.IMAGE.elements.altWarning, this.root);\n } else {\n hideElements(Selectors.IMAGE.elements.altWarning, this.root);\n }\n this.toggleAriaInvalid([Selectors.IMAGE.elements.alt, Selectors.IMAGE.elements.presentation], imageAltError);\n\n return imageAltError;\n }\n\n updateWarning() {\n const urlError = this.hasErrorUrlField();\n const imageAltError = this.hasErrorAltField();\n\n return urlError || imageAltError;\n }\n\n getImageContext() {\n // Check if there are any accessibility issues.\n if (this.updateWarning()) {\n return null;\n }\n\n const classList = [];\n const constrain = this.root.querySelector(Selectors.IMAGE.elements.constrain).checked;\n const sizeOriginal = this.root.querySelector(Selectors.IMAGE.elements.sizeOriginal).checked;\n if (constrain || sizeOriginal) {\n // If the Auto size checkbox is checked or the Original size is checked, then apply the responsive class.\n classList.push(Selectors.IMAGE.styles.responsive);\n } else {\n // Otherwise, remove it.\n classList.pop(Selectors.IMAGE.styles.responsive);\n }\n\n return {\n url: this.currentUrl,\n alt: this.root.querySelector(Selectors.IMAGE.elements.alt).value,\n width: this.root.querySelector(Selectors.IMAGE.elements.width).value,\n height: this.root.querySelector(Selectors.IMAGE.elements.height).value,\n presentation: this.root.querySelector(Selectors.IMAGE.elements.presentation).checked,\n customStyle: this.root.querySelector(Selectors.IMAGE.elements.customStyle).value,\n classlist: classList.join(' '),\n };\n }\n\n setImage() {\n const pendingPromise = new Pending('tiny_media:setImage');\n const url = this.currentUrl;\n if (url === '') {\n return;\n }\n\n // Check if there are any accessibility issues.\n if (this.updateWarning()) {\n pendingPromise.resolve();\n return;\n }\n\n // Check for invalid width or height.\n const width = this.root.querySelector(Selectors.IMAGE.elements.width).value;\n if (!isPercentageValue(width) && isNaN(parseInt(width, 10))) {\n this.root.querySelector(Selectors.IMAGE.elements.width).focus();\n pendingPromise.resolve();\n return;\n }\n\n const height = this.root.querySelector(Selectors.IMAGE.elements.height).value;\n if (!isPercentageValue(height) && isNaN(parseInt(height, 10))) {\n this.root.querySelector(Selectors.IMAGE.elements.height).focus();\n pendingPromise.resolve();\n return;\n }\n\n Templates.render('tiny_media/image', this.getImageContext())\n .then((html) => {\n this.editor.insertContent(html);\n this.currentModal.destroy();\n pendingPromise.resolve();\n\n return html;\n })\n .catch(error => {\n window.console.log(error);\n });\n }\n\n /**\n * Deletes the image after confirming with the user and loads the insert image page.\n */\n deleteImage() {\n Notification.deleteCancelPromise(\n getString('deleteimage', 'tiny_media'),\n getString('deleteimagewarning', 'tiny_media'),\n ).then(() => {\n hideElements(Selectors.IMAGE.elements.altWarning, this.root);\n // Removing the image in the preview will bring the user to the insert page.\n this.loadInsertImage();\n return;\n }).catch(error => {\n window.console.log(error);\n });\n }\n\n registerEventListeners() {\n const submitAction = this.root.querySelector(Selectors.IMAGE.actions.submit);\n submitAction.addEventListener('click', (e) => {\n e.preventDefault();\n this.setImage();\n });\n\n const deleteImageEle = this.root.querySelector(Selectors.IMAGE.actions.deleteImage);\n deleteImageEle.addEventListener('click', () => {\n this.deleteImage();\n });\n deleteImageEle.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\") {\n this.deleteImage();\n }\n });\n\n this.root.addEventListener('change', (e) => {\n const presentationEle = e.target.closest(Selectors.IMAGE.elements.presentation);\n if (presentationEle) {\n this.presentationChanged();\n }\n\n const constrainEle = e.target.closest(Selectors.IMAGE.elements.constrain);\n if (constrainEle) {\n this.autoAdjustSize();\n }\n\n const sizeOriginalEle = e.target.closest(Selectors.IMAGE.elements.sizeOriginal);\n if (sizeOriginalEle) {\n this.sizeChecked('original');\n }\n\n const sizeCustomEle = e.target.closest(Selectors.IMAGE.elements.sizeCustom);\n if (sizeCustomEle) {\n this.sizeChecked('custom');\n }\n });\n\n this.root.addEventListener('blur', (e) => {\n if (e.target.nodeType === Node.ELEMENT_NODE) {\n\n const presentationEle = e.target.closest(Selectors.IMAGE.elements.presentation);\n if (presentationEle) {\n this.presentationChanged();\n }\n }\n }, true);\n\n // Character count.\n this.root.addEventListener('keyup', (e) => {\n const altEle = e.target.closest(Selectors.IMAGE.elements.alt);\n if (altEle) {\n this.handleKeyupCharacterCount();\n }\n });\n\n this.root.addEventListener('input', (e) => {\n const widthEle = e.target.closest(Selectors.IMAGE.elements.width);\n if (widthEle) {\n // Avoid empty value.\n widthEle.value = widthEle.value === \"\" ? 0 : Number(widthEle.value);\n this.autoAdjustSize();\n }\n\n const heightEle = e.target.closest(Selectors.IMAGE.elements.height);\n if (heightEle) {\n // Avoid empty value.\n heightEle.value = heightEle.value === \"\" ? 0 : Number(heightEle.value);\n this.autoAdjustSize(true);\n }\n });\n }\n\n handleKeyupCharacterCount() {\n const alt = this.root.querySelector(Selectors.IMAGE.elements.alt).value;\n const current = this.root.querySelector('#currentcount');\n current.innerHTML = alt.length;\n }\n\n /**\n * Calculates the dimensions to fit a square into a specified box while maintaining aspect ratio.\n *\n * @param {number} squareWidth - The width of the square.\n * @param {number} squareHeight - The height of the square.\n * @param {number} boxWidth - The width of the box.\n * @param {number} boxHeight - The height of the box.\n * @returns {Object} An object with the new width and height of the square to fit in the box.\n */\n fitSquareIntoBox = (squareWidth, squareHeight, boxWidth, boxHeight) => {\n if (squareWidth < boxWidth && squareHeight < boxHeight) {\n // If the square is smaller than the box, keep its dimensions.\n return {\n width: squareWidth,\n height: squareHeight,\n };\n }\n // Calculate the scaling factor based on the minimum scaling required to fit in the box.\n const widthScaleFactor = boxWidth / squareWidth;\n const heightScaleFactor = boxHeight / squareHeight;\n const minScaleFactor = Math.min(widthScaleFactor, heightScaleFactor);\n // Scale the square's dimensions based on the aspect ratio and the minimum scaling factor.\n const newWidth = squareWidth * minScaleFactor;\n const newHeight = squareHeight * minScaleFactor;\n return {\n width: newWidth,\n height: newHeight,\n };\n };\n}\n"],"names":["ImageDetails","MediaBase","constructor","root","editor","currentModal","canShowFilePicker","canShowDropZone","currentUrl","image","WIDTH","HEIGHT","Selectors","IMAGE","type","setTitle","imageTypeChecked","presentationChanged","storeImageDimensions","this","setImageDimensions","registerEventListeners","async","templateContext","elementid","id","showfilepicker","showdropzone","bodyTemplate","template","body","insertImageBody","footerTemplate","footer","insertImageFooter","selector","Promise","all","then","ImageInsert","init","catch","error","window","console","log","imagePreviewBox","querySelector","elements","previewBox","preview","widthField","width","heightField","height","updateImageDimensions","boxWidth","clientWidth","boxHeight","clientHeight","dimensions","fitSquareIntoBox","value","style","getRoot","on","ModalEvents","shown","squareWidth","squareHeight","widthScaleFactor","heightScaleFactor","minScaleFactor","Math","min","mediaDimensions","DEFAULTS","currentWidth","element","getCurrentWidth","currentHeight","getCurrentHeight","setAttribute","src","display","constrain","checked","disabled","widthRatio","round","parseInt","heightRatio","sizeChecked","setSelectedSize","Number","presentation","alt","handleKeyupCharacterCount","isExternalUrl","RegExp","Config","wwwroot","test","url","setFilenameLabel","decodeURI","filename","split","pop","label","urlLabelEle","fileNameLabel","innerHTML","toggleAriaInvalid","selectors","predicate","forEach","querySelectorAll","hasErrorUrlField","urlError","urlWarning","hasErrorAltField","imageAltError","altWarning","updateWarning","getImageContext","classList","sizeOriginal","push","styles","responsive","customStyle","classlist","join","setImage","pendingPromise","Pending","resolve","isNaN","focus","render","html","insertContent","destroy","deleteImage","deleteCancelPromise","loadInsertImage","actions","submit","addEventListener","e","preventDefault","deleteImageEle","key","target","closest","autoAdjustSize","sizeCustom","nodeType","Node","ELEMENT_NODE","widthEle","heightEle","length"],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"imagedetails.min.js","sources":["../src/imagedetails.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 details class for Moodle.\n *\n * @module tiny_media/imagedetails\n * @copyright 2024 Meirza \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Config from 'core/config';\nimport ModalEvents from 'core/modal_events';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport Selectors from './selectors';\nimport Templates from 'core/templates';\nimport {getString} from 'core/str';\nimport {ImageInsert} from 'tiny_media/imageinsert';\nimport {MediaBase} from './mediabase';\nimport {\n body,\n footer,\n hideElements,\n showElements,\n isPercentageValue,\n} from './helpers';\n\nexport class ImageDetails extends MediaBase {\n DEFAULTS = {\n WIDTH: 160,\n HEIGHT: 160,\n };\n\n selectorType = Selectors.IMAGE.type;\n\n mediaDimensions = null;\n\n constructor(\n root,\n editor,\n currentModal,\n canShowFilePicker,\n canShowDropZone,\n currentUrl,\n image,\n ) {\n super();\n this.root = root;\n this.editor = editor;\n this.currentModal = currentModal;\n this.canShowFilePicker = canShowFilePicker;\n this.canShowDropZone = canShowDropZone;\n this.currentUrl = currentUrl;\n this.image = image;\n this.toggleMaxlengthFeedbackSuffix = false;\n }\n\n init = function() {\n this.currentModal.setTitle(getString('imagedetails', 'tiny_media'));\n this.imageTypeChecked();\n this.presentationChanged();\n this.storeImageDimensions(this.image);\n this.setImageDimensions();\n this.registerEventListeners();\n };\n\n /**\n * Loads and displays a preview image based on the provided URL, and handles image loading events.\n */\n loadInsertImage = async function() {\n const templateContext = {\n elementid: this.editor.id,\n showfilepicker: this.canShowFilePicker,\n showdropzone: this.canShowDropZone,\n bodyTemplate: Selectors.IMAGE.template.body.insertImageBody,\n footerTemplate: Selectors.IMAGE.template.footer.insertImageFooter,\n selector: Selectors.IMAGE.type,\n };\n\n Promise.all([body(templateContext, this.root), footer(templateContext, this.root)])\n .then(() => {\n const imageinsert = new ImageInsert(\n this.root,\n this.editor,\n this.currentModal,\n this.canShowFilePicker,\n this.canShowDropZone,\n );\n imageinsert.init();\n return;\n })\n .catch(error => {\n window.console.log(error);\n });\n };\n\n storeImageDimensions(image) {\n // Store dimensions of the raw image, falling back to defaults for images without dimensions (e.g. SVG).\n this.mediaDimensions = {\n width: image.width || this.DEFAULTS.WIDTH,\n height: image.height || this.DEFAULTS.HEIGHT,\n };\n\n const getCurrentWidth = (element) => {\n if (element.value === '') {\n element.value = this.mediaDimensions.width;\n }\n return element.value;\n };\n\n const getCurrentHeight = (element) => {\n if (element.value === '') {\n element.value = this.mediaDimensions.height;\n }\n return element.value;\n };\n\n const widthInput = this.root.querySelector(Selectors.IMAGE.elements.width);\n const currentWidth = getCurrentWidth(widthInput);\n\n const heightInput = this.root.querySelector(Selectors.IMAGE.elements.height);\n const currentHeight = getCurrentHeight(heightInput);\n\n const preview = this.root.querySelector(Selectors.IMAGE.elements.preview);\n preview.setAttribute('src', image.src);\n preview.style.display = '';\n\n // Ensure the checkbox always in unchecked status when an image loads at first.\n const constrain = this.root.querySelector(Selectors.IMAGE.elements.constrain);\n if (isPercentageValue(currentWidth) && isPercentageValue(currentHeight)) {\n constrain.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 constrain.disabled = 'disabled';\n } else {\n // This is the same as comparing to 3 decimal places.\n const widthRatio = Math.round(100 * parseInt(currentWidth, 10) / image.width);\n const heightRatio = Math.round(100 * parseInt(currentHeight, 10) / image.height);\n constrain.checked = widthRatio === heightRatio;\n }\n\n /**\n * Sets the selected size option based on current width and height values.\n *\n * @param {number} currentWidth - The current width value.\n * @param {number} currentHeight - The current height value.\n */\n const setSelectedSize = (currentWidth, currentHeight) => {\n if (this.mediaDimensions.width === currentWidth &&\n this.mediaDimensions.height === currentHeight\n ) {\n this.currentWidth = this.mediaDimensions.width;\n this.currentHeight = this.mediaDimensions.height;\n this.sizeChecked('original');\n } else {\n this.currentWidth = currentWidth;\n this.currentHeight = currentHeight;\n this.sizeChecked('custom');\n }\n };\n\n setSelectedSize(Number(currentWidth), Number(currentHeight));\n }\n\n /**\n * Sets the dimensions of the image preview element based on user input and constraints.\n */\n setImageDimensions = () => {\n const imagePreviewBox = this.root.querySelector(Selectors.IMAGE.elements.previewBox);\n const image = this.root.querySelector(Selectors.IMAGE.elements.preview);\n const widthField = this.root.querySelector(Selectors.IMAGE.elements.width);\n const heightField = this.root.querySelector(Selectors.IMAGE.elements.height);\n\n const updateImageDimensions = () => {\n // Get the latest dimensions of the preview box for responsiveness.\n const boxWidth = imagePreviewBox.clientWidth;\n const boxHeight = imagePreviewBox.clientHeight;\n // Get the new width and height for the image.\n const dimensions = this.fitSquareIntoBox(widthField.value, heightField.value, boxWidth, boxHeight);\n image.style.width = `${dimensions.width}px`;\n image.style.height = `${dimensions.height}px`;\n };\n // If the client size is zero, then get the new dimensions once the modal is shown.\n if (imagePreviewBox.clientWidth === 0) {\n // Call the shown event.\n this.currentModal.getRoot().on(ModalEvents.shown, () => {\n updateImageDimensions();\n });\n } else {\n updateImageDimensions();\n }\n };\n\n /**\n * Handles changes in the image presentation checkbox and enables/disables the image alt text input accordingly.\n */\n async presentationChanged() {\n const presentation = this.root.querySelector(Selectors.IMAGE.elements.presentation);\n const alt = this.root.querySelector(Selectors.IMAGE.elements.alt);\n alt.disabled = presentation.checked;\n\n // Counting the image description characters.\n await this.handleKeyupCharacterCount();\n }\n\n /**\n * This function checks whether an image URL is local (within the same website's domain) or external (from an external source).\n * Depending on the result, it dynamically updates the visibility and content of HTML elements in a user interface.\n * If the image is local then we only show it's filename.\n * If the image is external then it will show full URL and it can be updated.\n */\n imageTypeChecked() {\n const regex = new RegExp(`${Config.wwwroot}`);\n\n // True if the URL is from external, otherwise false.\n const isExternalUrl = regex.test(this.currentUrl) === false;\n\n // Hide the URL input.\n hideElements(Selectors.IMAGE.elements.url, this.root);\n\n if (!isExternalUrl) {\n // Split the URL by '/' to get an array of segments.\n const segments = this.currentUrl.split('/');\n // Get the last segment, which should be the filename.\n const filename = segments.pop().split('?')[0];\n // Show the file name.\n this.setFilenameLabel(decodeURI(filename));\n } else {\n\n this.setFilenameLabel(decodeURI(this.currentUrl));\n }\n }\n\n /**\n * Set the string for the URL label element.\n *\n * @param {string} label - The label text to set.\n */\n setFilenameLabel(label) {\n const urlLabelEle = this.root.querySelector(Selectors.IMAGE.elements.fileNameLabel);\n if (urlLabelEle) {\n urlLabelEle.innerHTML = label;\n urlLabelEle.setAttribute(\"title\", label);\n }\n }\n\n toggleAriaInvalid(selectors, predicate) {\n selectors.forEach((selector) => {\n const elements = this.root.querySelectorAll(selector);\n elements.forEach((element) => element.setAttribute('aria-invalid', predicate));\n });\n }\n\n hasErrorUrlField() {\n const urlError = this.currentUrl === '';\n if (urlError) {\n showElements(Selectors.IMAGE.elements.urlWarning, this.root);\n } else {\n hideElements(Selectors.IMAGE.elements.urlWarning, this.root);\n }\n this.toggleAriaInvalid([Selectors.IMAGE.elements.url], urlError);\n\n return urlError;\n }\n\n hasErrorAltField() {\n const alt = this.root.querySelector(Selectors.IMAGE.elements.alt).value;\n const presentation = this.root.querySelector(Selectors.IMAGE.elements.presentation).checked;\n const imageAltError = alt === '' && !presentation;\n if (imageAltError) {\n showElements(Selectors.IMAGE.elements.altWarning, this.root);\n } else {\n hideElements(Selectors.IMAGE.elements.altWarning, this.root);\n }\n this.toggleAriaInvalid([Selectors.IMAGE.elements.alt, Selectors.IMAGE.elements.presentation], imageAltError);\n\n return imageAltError;\n }\n\n updateWarning() {\n const urlError = this.hasErrorUrlField();\n const imageAltError = this.hasErrorAltField();\n\n return urlError || imageAltError;\n }\n\n getImageContext() {\n // Check if there are any accessibility issues.\n if (this.updateWarning()) {\n return null;\n }\n\n const classList = [];\n const constrain = this.root.querySelector(Selectors.IMAGE.elements.constrain).checked;\n const sizeOriginal = this.root.querySelector(Selectors.IMAGE.elements.sizeOriginal).checked;\n if (constrain || sizeOriginal) {\n // If the Auto size checkbox is checked or the Original size is checked, then apply the responsive class.\n classList.push(Selectors.IMAGE.styles.responsive);\n } else {\n // Otherwise, remove it.\n classList.pop(Selectors.IMAGE.styles.responsive);\n }\n\n return {\n url: this.currentUrl,\n alt: this.root.querySelector(Selectors.IMAGE.elements.alt).value,\n width: this.root.querySelector(Selectors.IMAGE.elements.width).value,\n height: this.root.querySelector(Selectors.IMAGE.elements.height).value,\n presentation: this.root.querySelector(Selectors.IMAGE.elements.presentation).checked,\n customStyle: this.root.querySelector(Selectors.IMAGE.elements.customStyle).value,\n classlist: classList.join(' '),\n };\n }\n\n setImage() {\n const pendingPromise = new Pending('tiny_media:setImage');\n const url = this.currentUrl;\n if (url === '') {\n return;\n }\n\n // Check if there are any accessibility issues.\n if (this.updateWarning()) {\n pendingPromise.resolve();\n return;\n }\n\n // Check for invalid width or height.\n const width = this.root.querySelector(Selectors.IMAGE.elements.width).value;\n if (!isPercentageValue(width) && isNaN(parseInt(width, 10))) {\n this.root.querySelector(Selectors.IMAGE.elements.width).focus();\n pendingPromise.resolve();\n return;\n }\n\n const height = this.root.querySelector(Selectors.IMAGE.elements.height).value;\n if (!isPercentageValue(height) && isNaN(parseInt(height, 10))) {\n this.root.querySelector(Selectors.IMAGE.elements.height).focus();\n pendingPromise.resolve();\n return;\n }\n\n Templates.render('tiny_media/image', this.getImageContext())\n .then((html) => {\n this.editor.insertContent(html);\n this.currentModal.destroy();\n pendingPromise.resolve();\n\n return html;\n })\n .catch(error => {\n window.console.log(error);\n });\n }\n\n /**\n * Deletes the image after confirming with the user and loads the insert image page.\n */\n deleteImage() {\n Notification.deleteCancelPromise(\n getString('deleteimage', 'tiny_media'),\n getString('deleteimagewarning', 'tiny_media'),\n ).then(() => {\n hideElements(Selectors.IMAGE.elements.altWarning, this.root);\n // Removing the image in the preview will bring the user to the insert page.\n this.loadInsertImage();\n return;\n }).catch(error => {\n window.console.log(error);\n });\n }\n\n registerEventListeners() {\n const submitAction = this.root.querySelector(Selectors.IMAGE.actions.submit);\n submitAction.addEventListener('click', (e) => {\n e.preventDefault();\n this.setImage();\n });\n\n const deleteImageEle = this.root.querySelector(Selectors.IMAGE.actions.deleteImage);\n deleteImageEle.addEventListener('click', () => {\n this.deleteImage();\n });\n deleteImageEle.addEventListener(\"keydown\", (e) => {\n if (e.key === \"Enter\") {\n this.deleteImage();\n }\n });\n\n this.root.addEventListener('change', async(e) => {\n const presentationEle = e.target.closest(Selectors.IMAGE.elements.presentation);\n if (presentationEle) {\n await this.presentationChanged();\n }\n\n const constrainEle = e.target.closest(Selectors.IMAGE.elements.constrain);\n if (constrainEle) {\n this.autoAdjustSize();\n }\n\n const sizeOriginalEle = e.target.closest(Selectors.IMAGE.elements.sizeOriginal);\n if (sizeOriginalEle) {\n this.sizeChecked('original');\n }\n\n const sizeCustomEle = e.target.closest(Selectors.IMAGE.elements.sizeCustom);\n if (sizeCustomEle) {\n this.sizeChecked('custom');\n }\n });\n\n this.root.addEventListener('blur', async(e) => {\n if (e.target.nodeType === Node.ELEMENT_NODE) {\n\n const presentationEle = e.target.closest(Selectors.IMAGE.elements.presentation);\n if (presentationEle) {\n await this.presentationChanged();\n }\n }\n }, true);\n\n // Character count.\n this.root.addEventListener('keyup', async(e) => {\n const altEle = e.target.closest(Selectors.IMAGE.elements.alt);\n if (altEle) {\n await this.handleKeyupCharacterCount();\n }\n });\n\n this.root.addEventListener('input', (e) => {\n const widthEle = e.target.closest(Selectors.IMAGE.elements.width);\n if (widthEle) {\n // Avoid empty value.\n widthEle.value = widthEle.value === \"\" ? 0 : Number(widthEle.value);\n this.autoAdjustSize();\n }\n\n const heightEle = e.target.closest(Selectors.IMAGE.elements.height);\n if (heightEle) {\n // Avoid empty value.\n heightEle.value = heightEle.value === \"\" ? 0 : Number(heightEle.value);\n this.autoAdjustSize(true);\n }\n });\n }\n\n async handleKeyupCharacterCount() {\n const altField = this.root.querySelector(Selectors.IMAGE.elements.alt);\n const alt = altField.value;\n const current = this.root.querySelector('#currentcount');\n current.innerHTML = alt.length;\n const maxLength = altField.getAttribute('maxlength');\n const maxLengthFeedback = document.getElementById('maxlength_feedback');\n if (alt.length >= maxLength) {\n maxLengthFeedback.textContent = await getString('maxlengthreached', 'core', maxLength);\n\n // Clever (or hacky?;p) way to ensure that the feedback message is announced to screen readers.\n const suffix = this.toggleMaxlengthFeedbackSuffix ? '' : '.';\n maxLengthFeedback.textContent += suffix;\n this.toggleMaxlengthFeedbackSuffix = !this.toggleMaxlengthFeedbackSuffix;\n\n // Clear the feedback message after 4 seconds. This is similar to the default timeout of toast messages\n // before disappearing from view. It is important to clear the message to prevent screen reader users from navigating\n // into this region and avoiding confusion.\n setTimeout(() => {\n maxLengthFeedback.textContent = '';\n }, 4000);\n }\n }\n\n /**\n * Calculates the dimensions to fit a square into a specified box while maintaining aspect ratio.\n *\n * @param {number} squareWidth - The width of the square.\n * @param {number} squareHeight - The height of the square.\n * @param {number} boxWidth - The width of the box.\n * @param {number} boxHeight - The height of the box.\n * @returns {Object} An object with the new width and height of the square to fit in the box.\n */\n fitSquareIntoBox = (squareWidth, squareHeight, boxWidth, boxHeight) => {\n if (squareWidth < boxWidth && squareHeight < boxHeight) {\n // If the square is smaller than the box, keep its dimensions.\n return {\n width: squareWidth,\n height: squareHeight,\n };\n }\n // Calculate the scaling factor based on the minimum scaling required to fit in the box.\n const widthScaleFactor = boxWidth / squareWidth;\n const heightScaleFactor = boxHeight / squareHeight;\n const minScaleFactor = Math.min(widthScaleFactor, heightScaleFactor);\n // Scale the square's dimensions based on the aspect ratio and the minimum scaling factor.\n const newWidth = squareWidth * minScaleFactor;\n const newHeight = squareHeight * minScaleFactor;\n return {\n width: newWidth,\n height: newHeight,\n };\n };\n}\n"],"names":["ImageDetails","MediaBase","constructor","root","editor","currentModal","canShowFilePicker","canShowDropZone","currentUrl","image","WIDTH","HEIGHT","Selectors","IMAGE","type","setTitle","imageTypeChecked","presentationChanged","storeImageDimensions","this","setImageDimensions","registerEventListeners","async","templateContext","elementid","id","showfilepicker","showdropzone","bodyTemplate","template","body","insertImageBody","footerTemplate","footer","insertImageFooter","selector","Promise","all","then","ImageInsert","init","catch","error","window","console","log","imagePreviewBox","querySelector","elements","previewBox","preview","widthField","width","heightField","height","updateImageDimensions","boxWidth","clientWidth","boxHeight","clientHeight","dimensions","fitSquareIntoBox","value","style","getRoot","on","ModalEvents","shown","squareWidth","squareHeight","widthScaleFactor","heightScaleFactor","minScaleFactor","Math","min","toggleMaxlengthFeedbackSuffix","mediaDimensions","DEFAULTS","currentWidth","element","getCurrentWidth","currentHeight","getCurrentHeight","setAttribute","src","display","constrain","checked","disabled","widthRatio","round","parseInt","heightRatio","sizeChecked","setSelectedSize","Number","presentation","alt","handleKeyupCharacterCount","isExternalUrl","RegExp","Config","wwwroot","test","url","setFilenameLabel","decodeURI","filename","split","pop","label","urlLabelEle","fileNameLabel","innerHTML","toggleAriaInvalid","selectors","predicate","forEach","querySelectorAll","hasErrorUrlField","urlError","urlWarning","hasErrorAltField","imageAltError","altWarning","updateWarning","getImageContext","classList","sizeOriginal","push","styles","responsive","customStyle","classlist","join","setImage","pendingPromise","Pending","resolve","isNaN","focus","render","html","insertContent","destroy","deleteImage","deleteCancelPromise","loadInsertImage","actions","submit","addEventListener","e","preventDefault","deleteImageEle","key","target","closest","autoAdjustSize","sizeCustom","nodeType","Node","ELEMENT_NODE","widthEle","heightEle","altField","length","maxLength","getAttribute","maxLengthFeedback","document","getElementById","textContent","suffix","setTimeout"],"mappings":"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"}
\ No newline at end of file
diff --git a/lib/editor/tiny/plugins/media/amd/src/imagedetails.js b/lib/editor/tiny/plugins/media/amd/src/imagedetails.js
index 14d1d4f3329..648b3121d5c 100644
--- a/lib/editor/tiny/plugins/media/amd/src/imagedetails.js
+++ b/lib/editor/tiny/plugins/media/amd/src/imagedetails.js
@@ -65,6 +65,7 @@ export class ImageDetails extends MediaBase {
this.canShowDropZone = canShowDropZone;
this.currentUrl = currentUrl;
this.image = image;
+ this.toggleMaxlengthFeedbackSuffix = false;
}
init = function() {
@@ -206,13 +207,13 @@ export class ImageDetails extends MediaBase {
/**
* Handles changes in the image presentation checkbox and enables/disables the image alt text input accordingly.
*/
- presentationChanged() {
+ async presentationChanged() {
const presentation = this.root.querySelector(Selectors.IMAGE.elements.presentation);
const alt = this.root.querySelector(Selectors.IMAGE.elements.alt);
alt.disabled = presentation.checked;
// Counting the image description characters.
- this.handleKeyupCharacterCount();
+ await this.handleKeyupCharacterCount();
}
/**
@@ -399,10 +400,10 @@ export class ImageDetails extends MediaBase {
}
});
- this.root.addEventListener('change', (e) => {
+ this.root.addEventListener('change', async(e) => {
const presentationEle = e.target.closest(Selectors.IMAGE.elements.presentation);
if (presentationEle) {
- this.presentationChanged();
+ await this.presentationChanged();
}
const constrainEle = e.target.closest(Selectors.IMAGE.elements.constrain);
@@ -421,21 +422,21 @@ export class ImageDetails extends MediaBase {
}
});
- this.root.addEventListener('blur', (e) => {
+ this.root.addEventListener('blur', async(e) => {
if (e.target.nodeType === Node.ELEMENT_NODE) {
const presentationEle = e.target.closest(Selectors.IMAGE.elements.presentation);
if (presentationEle) {
- this.presentationChanged();
+ await this.presentationChanged();
}
}
}, true);
// Character count.
- this.root.addEventListener('keyup', (e) => {
+ this.root.addEventListener('keyup', async(e) => {
const altEle = e.target.closest(Selectors.IMAGE.elements.alt);
if (altEle) {
- this.handleKeyupCharacterCount();
+ await this.handleKeyupCharacterCount();
}
});
@@ -456,10 +457,28 @@ export class ImageDetails extends MediaBase {
});
}
- handleKeyupCharacterCount() {
- const alt = this.root.querySelector(Selectors.IMAGE.elements.alt).value;
+ async handleKeyupCharacterCount() {
+ const altField = this.root.querySelector(Selectors.IMAGE.elements.alt);
+ const alt = altField.value;
const current = this.root.querySelector('#currentcount');
current.innerHTML = alt.length;
+ const maxLength = altField.getAttribute('maxlength');
+ const maxLengthFeedback = document.getElementById('maxlength_feedback');
+ if (alt.length >= maxLength) {
+ maxLengthFeedback.textContent = await getString('maxlengthreached', 'core', maxLength);
+
+ // Clever (or hacky?;p) way to ensure that the feedback message is announced to screen readers.
+ const suffix = this.toggleMaxlengthFeedbackSuffix ? '' : '.';
+ maxLengthFeedback.textContent += suffix;
+ this.toggleMaxlengthFeedbackSuffix = !this.toggleMaxlengthFeedbackSuffix;
+
+ // Clear the feedback message after 4 seconds. This is similar to the default timeout of toast messages
+ // before disappearing from view. It is important to clear the message to prevent screen reader users from navigating
+ // into this region and avoiding confusion.
+ setTimeout(() => {
+ maxLengthFeedback.textContent = '';
+ }, 4000);
+ }
}
/**
diff --git a/lib/editor/tiny/plugins/media/templates/insert_image_modal_details.mustache b/lib/editor/tiny/plugins/media/templates/insert_image_modal_details.mustache
index 8af00da39fe..b7f79942214 100644
--- a/lib/editor/tiny/plugins/media/templates/insert_image_modal_details.mustache
+++ b/lib/editor/tiny/plugins/media/templates/insert_image_modal_details.mustache
@@ -58,6 +58,7 @@
0
/
{{maxlengthalt}}
+