diff --git a/lib/editor/tiny/plugins/media/amd/build/image.min.js b/lib/editor/tiny/plugins/media/amd/build/image.min.js index 4f7e2cc1d76..d57ca167f92 100644 --- a/lib/editor/tiny/plugins/media/amd/build/image.min.js +++ b/lib/editor/tiny/plugins/media/amd/build/image.min.js @@ -1,3 +1,3 @@ -define("tiny_media/image",["exports","core/templates","core/str","core/pending","editor_tiny/utils","./selectors","./imagemodal","./options","editor_tiny/options"],(function(_exports,_templates,_str,_pending,_utils,_selectors,_imagemodal,_options,_options2){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_templates=_interopRequireDefault(_templates),_pending=_interopRequireDefault(_pending),_selectors=_interopRequireDefault(_selectors),_imagemodal=_interopRequireDefault(_imagemodal);return _exports.default=class{constructor(editor){_defineProperty(this,"DEFAULTS",{WIDTH:160,HEIGHT:160}),_defineProperty(this,"form",null),_defineProperty(this,"rawImageDimensions",null),_defineProperty(this,"canShowFilePicker",!1),_defineProperty(this,"editor",null),_defineProperty(this,"currentModal",null),_defineProperty(this,"selectedImage",null);const permissions=(0,_options.getImagePermissions)(editor);this.canShowFilePicker=permissions.filepicker&&void 0!==(0,_options2.getFilePicker)(editor,"image"),this.editor=editor}async displayDialogue(){this.rawImageDimensions=null;const currentImageData=await this.getCurrentImageData();this.currentModal=await _imagemodal.default.create({title:(0,_str.getString)("imageproperties","tiny_media"),templateContext:await this.getTemplateContext(currentImageData)}),currentImageData&¤tImageData.src&&this.loadPreviewImage(currentImageData.src),await this.registerEventListeners()}async getTemplateContext(data){return{elementid:this.editor.id,showfilepicker:this.canShowFilePicker,...data}}async getCurrentImageData(){const selectedImageProperties=this.getSelectedImageProperties();if(!selectedImageProperties)return{};const properties={...selectedImageProperties};return properties.src&&(properties.haspreview=!0),properties.alt||(properties.presentation=!0),properties}filePickerCallback(params,self){if(params.url){self.form.querySelector(_selectors.default.IMAGE.elements.url).value=params.url,self.form.querySelector(_selectors.default.IMAGE.elements.width).value="",self.form.querySelector(_selectors.default.IMAGE.elements.height).value="",self.loadPreviewImage(params.url)}}storeImageDimensions(image){this.rawImageDimensions={width:image.width||this.DEFAULTS.WIDTH,height:image.height||this.DEFAULTS.HEIGHT};const currentWidth=(element=>(""===element.value&&(element.value=this.rawImageDimensions.width),element.value))(this.form.querySelector(_selectors.default.IMAGE.elements.width)),currentHeight=(element=>(""===element.value&&(element.value=this.rawImageDimensions.height),element.value))(this.form.querySelector(_selectors.default.IMAGE.elements.height)),preview=this.form.querySelector(_selectors.default.IMAGE.elements.preview);preview.setAttribute("src",image.src),preview.style.display="inline";const constrain=this.form.querySelector(_selectors.default.IMAGE.elements.constrain);if(this.isPercentageValue(currentWidth)&&this.isPercentageValue(currentHeight))constrain.checked=currentWidth===currentHeight;else if(0===image.width||0===image.height)constrain.disabled="disabled";else{const widthRatio=Math.round(1e3*parseInt(currentWidth,10)/image.width),heightRatio=Math.round(1e3*parseInt(currentHeight,10)/image.height);constrain.checked=widthRatio===heightRatio}}loadPreviewImage(url){const image=new Image;image.addEventListener("error",(()=>{this.form.querySelector(_selectors.default.IMAGE.elements.preview).style.display="none"})),image.addEventListener("load",(()=>{this.storeImageDimensions(image),this.autoAdjustSize()})),image.src=url}urlChanged(){const input=this.form.querySelector(_selectors.default.IMAGE.elements.url);input.value&&this.loadPreviewImage(input.value)}hasErrorUrlField(){const urlError=""===this.form.querySelector(_selectors.default.IMAGE.elements.url).value;return this.toggleVisibility(_selectors.default.IMAGE.elements.urlWarning,urlError),this.toggleAriaInvalid([_selectors.default.IMAGE.elements.url],urlError),urlError}hasErrorAltField(){const alt=this.form.querySelector(_selectors.default.IMAGE.elements.alt).value,presentation=this.form.querySelector(_selectors.default.IMAGE.elements.presentation).checked,imageAltError=""===alt&&!presentation;return this.toggleVisibility(_selectors.default.IMAGE.elements.altWarning,imageAltError),this.toggleAriaInvalid([_selectors.default.IMAGE.elements.alt,_selectors.default.IMAGE.elements.presentation],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)))}))}updateWarning(){const urlError=this.hasErrorUrlField(),imageAltError=this.hasErrorAltField();return urlError||imageAltError}getImageContext(){if(this.updateWarning())return null;const classList=[];return this.form.querySelector(_selectors.default.IMAGE.elements.constrain).value&&classList.push(_selectors.default.IMAGE.styles.responsive),{url:this.form.querySelector(_selectors.default.IMAGE.elements.url).value,alt:this.form.querySelector(_selectors.default.IMAGE.elements.alt).value,width:this.form.querySelector(_selectors.default.IMAGE.elements.width).value,height:this.form.querySelector(_selectors.default.IMAGE.elements.height).value,presentation:this.form.querySelector(_selectors.default.IMAGE.elements.presentation).checked,customStyle:this.form.querySelector(_selectors.default.IMAGE.elements.customStyle).value,classlist:classList.join(" ")}}setImage(){const pendingPromise=new _pending.default("tiny_media:setImage");if(""===this.form.querySelector(_selectors.default.IMAGE.elements.url).value)return;if(this.updateWarning())return void pendingPromise.resolve();const width=this.form.querySelector(_selectors.default.IMAGE.elements.width).value;if(!this.isPercentageValue(width)&&isNaN(parseInt(width,10)))return this.form.querySelector(_selectors.default.IMAGE.elements.width).focus(),void pendingPromise.resolve();const height=this.form.querySelector(_selectors.default.IMAGE.elements.height).value;if(!this.isPercentageValue(height)&&isNaN(parseInt(height,10)))return this.form.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()}handleKeyupCharacterCount(){const alt=this.form.querySelector(_selectors.default.IMAGE.elements.alt).value;this.form.querySelector("#currentcount").innerHTML=alt.length}autoAdjustSize(){let forceHeight=arguments.length>0&&void 0!==arguments[0]&&arguments[0];if(!this.rawImageDimensions)return;const widthField=this.form.querySelector(_selectors.default.IMAGE.elements.width),heightField=this.form.querySelector(_selectors.default.IMAGE.elements.height),normalizeFieldData=fieldData=>(fieldData.isPercentageValue=!!this.isPercentageValue(fieldData.field.value),fieldData.isPercentageValue?(fieldData.percentValue=parseInt(fieldData.field.value,10),fieldData.pixelSize=this.rawImageDimensions[fieldData.type]/100*fieldData.percentValue):(fieldData.pixelSize=parseInt(fieldData.field.value,10),fieldData.percentValue=fieldData.pixelSize/this.rawImageDimensions[fieldData.type]*100),fieldData),imagePreview=this.form.querySelector(_selectors.default.IMAGE.elements.preview);imagePreview.style.width="",imagePreview.style.height="";const constrainField=this.form.querySelector(_selectors.default.IMAGE.elements.constrain),keyField=(()=>{const currentValue=forceHeight?{field:heightField,type:"height"}:{field:widthField,type:"width"};return""===currentValue.field.value&&(currentValue.field.value=this.rawImageDimensions[currentValue.type]),normalizeFieldData(currentValue)})(),relativeField=normalizeFieldData(forceHeight?{field:widthField,type:"width"}:{field:heightField,type:"height"});constrainField.checked&&(keyField.isPercentageValue?(relativeField.field.value=keyField.field.value,relativeField.percentValue=keyField.percentValue):(relativeField.pixelSize=Math.round(keyField.pixelSize/this.rawImageDimensions[keyField.type]*this.rawImageDimensions[relativeField.type]),relativeField.field.value=relativeField.pixelSize)),function(image,keyField,relativeField){let forceHeight=arguments.length>3&&void 0!==arguments[3]&&arguments[3];const getStyleValue=field=>field.isPercentageValue?"".concat(field.percentValue,"%"):"".concat(field.pixelSize,"px");forceHeight?"width"===keyField.type?image.style.width=getStyleValue(keyField):image.style.width=getStyleValue(relativeField):(image.style[keyField.type]=getStyleValue(keyField),image.style[relativeField.type]=getStyleValue(relativeField))}(imagePreview,keyField,relativeField,!!constrainField.checked)}getSelectedImageProperties(){const image=this.getSelectedImage();if(!image)return this.selectedImage=null,null;const properties={src:null,alt:null,width:null,height:null,presentation:!1};this.selectedImage=image,properties.customStyle=image.style.cssText;const width=(image=>this.isPercentageValue(String(image.width))?image.width:parseInt(image.width,10))(image);0!==width&&(properties.width=width);const height=(image=>this.isPercentageValue(String(image.height))?image.height:parseInt(image.height,10))(image);return 0!==height&&(properties.height=height),properties.src=image.getAttribute("src"),properties.alt=image.getAttribute("alt")||"",properties.presentation="presentation"===image.getAttribute("role"),properties}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.toUpperCase()||this.isPlaceholderImage(imgElm))?null:imgElm}isPlaceholderImage(imgElm){return"IMG"===imgElm.nodeName.toUpperCase()&&(imgElm.hasAttribute("data-mce-object")||imgElm.hasAttribute("data-mce-placeholder"))}isPercentageValue(value){return value.match(/\d+%/)}async registerEventListeners(){await this.currentModal.getBody();const root=this.currentModal.getRoot()[0];this.form=root.querySelector(_selectors.default.IMAGE.elements.form),root.addEventListener("click",(e=>{const submitAction=e.target.closest(_selectors.default.IMAGE.actions.submit),imageBrowserAction=e.target.closest(_selectors.default.IMAGE.actions.imageBrowser);submitAction&&(e.preventDefault(),this.setImage()),imageBrowserAction&&this.canShowFilePicker&&(e.preventDefault(),(0,_utils.displayFilepicker)(this.editor,"image").then((params=>{this.filePickerCallback(params,this)})).catch())})),root.addEventListener("change",(e=>{e.target.closest(_selectors.default.IMAGE.elements.url)&&this.hasErrorUrlField();e.target.closest(_selectors.default.IMAGE.elements.presentation)&&this.hasErrorAltField();e.target.closest(_selectors.default.IMAGE.elements.constrain)&&this.autoAdjustSize(!0)})),root.addEventListener("blur",(e=>{if(e.target.nodeType===Node.ELEMENT_NODE){e.target.closest(_selectors.default.IMAGE.elements.url)&&this.urlChanged();e.target.closest(_selectors.default.IMAGE.elements.alt)&&this.hasErrorAltField();e.target.closest(_selectors.default.IMAGE.elements.width)&&this.autoAdjustSize();e.target.closest(_selectors.default.IMAGE.elements.height)&&this.autoAdjustSize(!0)}}),!0),root.addEventListener("keyup",(e=>{e.target.closest(_selectors.default.IMAGE.elements.alt)&&this.handleKeyupCharacterCount()}))}},_exports.default})); +define("tiny_media/image",["exports","./imagemodal","./options","editor_tiny/options","tiny_media/imageinsert","tiny_media/imagehelpers"],(function(_exports,_imagemodal,_options,_options2,_imageinsert,_imagehelpers){var obj;function _defineProperty(obj,key,value){return key in obj?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,obj}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.default=void 0,_imagemodal=(obj=_imagemodal)&&obj.__esModule?obj:{default:obj};return _exports.default=class{constructor(editor){_defineProperty(this,"canShowFilePicker",!1),_defineProperty(this,"editor",null),_defineProperty(this,"currentModal",null),_defineProperty(this,"root",null),_defineProperty(this,"loadInsertImage",(async function(){const templateContext={elementid:this.editor.id,showfilepicker:this.canShowFilePicker,showdropzone:this.canShowDropZone};Promise.all([(0,_imagehelpers.bodyImageInsert)(templateContext,this.root),(0,_imagehelpers.footerImageInsert)(templateContext,this.root)]).then((()=>{new _imageinsert.ImageInsert(this.root,this.editor,this.currentModal,this.canShowFilePicker,this.canShowDropZone).init()})).catch((error=>{window.console.log(error)}))}));const permissions=(0,_options.getImagePermissions)(editor),options=(0,_options2.getFilePicker)(editor,"image");this.canShowFilePicker=permissions.filepicker&&void 0!==options&&Object.keys(options.repositories).length>0,this.canShowDropZone=Object.values(options.repositories).some((repository=>"upload"===repository.type)),this.editor=editor}async displayDialogue(){this.currentModal=await _imagemodal.default.create(),this.root=this.currentModal.getRoot()[0],this.loadInsertImage()}},_exports.default})); //# 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 index 584ca92bac2..5e883c66019 100644 --- a/lib/editor/tiny/plugins/media/amd/build/image.min.js.map +++ b/lib/editor/tiny/plugins/media/amd/build/image.min.js.map @@ -1 +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 {getString} from 'core/str';\nimport Pending from 'core/pending';\nimport {displayFilepicker} from 'editor_tiny/utils';\nimport Selectors from './selectors';\nimport ImageModal from './imagemodal';\nimport {getImagePermissions} from './options';\nimport {getFilePicker} from 'editor_tiny/options';\n\nexport default class MediaImage {\n DEFAULTS = {\n WIDTH: 160,\n HEIGHT: 160,\n };\n\n form = null;\n rawImageDimensions = null;\n canShowFilePicker = false;\n editor = null;\n currentModal = null;\n selectedImage = null;\n\n constructor(editor) {\n const permissions = getImagePermissions(editor);\n\n // Indicates whether the file picker can be shown.\n this.canShowFilePicker = permissions.filepicker && (typeof getFilePicker(editor, 'image') !== 'undefined');\n\n this.editor = editor;\n }\n\n async displayDialogue() {\n // Reset the image dimensions.\n this.rawImageDimensions = null;\n\n const currentImageData = await this.getCurrentImageData();\n this.currentModal = await ImageModal.create({\n title: getString('imageproperties', 'tiny_media'),\n templateContext: await this.getTemplateContext(currentImageData),\n });\n\n if (currentImageData && currentImageData.src) {\n this.loadPreviewImage(currentImageData.src);\n }\n\n await this.registerEventListeners();\n }\n\n async getTemplateContext(data) {\n return {\n elementid: this.editor.id,\n showfilepicker: this.canShowFilePicker,\n ...data,\n };\n }\n\n async getCurrentImageData() {\n const selectedImageProperties = this.getSelectedImageProperties();\n if (!selectedImageProperties) {\n return {};\n }\n\n const properties = {...selectedImageProperties};\n\n if (properties.src) {\n properties.haspreview = true;\n }\n\n if (!properties.alt) {\n properties.presentation = true;\n }\n\n return properties;\n }\n\n filePickerCallback(params, self) {\n if (params.url) {\n const input = self.form.querySelector(Selectors.IMAGE.elements.url);\n input.value = params.url;\n\n // Auto set the width and height.\n self.form.querySelector(Selectors.IMAGE.elements.width).value = '';\n self.form.querySelector(Selectors.IMAGE.elements.height).value = '';\n\n // Load the preview image.\n self.loadPreviewImage(params.url);\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.rawImageDimensions = {\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.rawImageDimensions.width;\n }\n return element.value;\n };\n const getCurrentHeight = (element) => {\n if (element.value === '') {\n element.value = this.rawImageDimensions.height;\n }\n return element.value;\n };\n\n const widthInput = this.form.querySelector(Selectors.IMAGE.elements.width);\n const currentWidth = getCurrentWidth(widthInput);\n\n const heightInput = this.form.querySelector(Selectors.IMAGE.elements.height);\n const currentHeight = getCurrentHeight(heightInput);\n\n const preview = this.form.querySelector(Selectors.IMAGE.elements.preview);\n preview.setAttribute('src', image.src);\n preview.style.display = 'inline';\n\n const constrain = this.form.querySelector(Selectors.IMAGE.elements.constrain);\n if (this.isPercentageValue(currentWidth) && this.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(1000 * parseInt(currentWidth, 10) / image.width);\n const heightRatio = Math.round(1000 * parseInt(currentHeight, 10) / image.height);\n constrain.checked = widthRatio === heightRatio;\n }\n }\n\n loadPreviewImage(url) {\n const image = new Image();\n\n image.addEventListener('error', () => {\n const preview = this.form.querySelector(Selectors.IMAGE.elements.preview);\n preview.style.display = 'none';\n });\n image.addEventListener('load', () => {\n this.storeImageDimensions(image);\n this.autoAdjustSize();\n });\n\n image.src = url;\n }\n\n urlChanged() {\n const input = this.form.querySelector(Selectors.IMAGE.elements.url);\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(Selectors.IMAGE.elements.url).value;\n const urlError = url === '';\n this.toggleVisibility(Selectors.IMAGE.elements.urlWarning, urlError);\n this.toggleAriaInvalid([Selectors.IMAGE.elements.url], urlError);\n\n return urlError;\n }\n\n hasErrorAltField() {\n const alt = this.form.querySelector(Selectors.IMAGE.elements.alt).value;\n const presentation = this.form.querySelector(Selectors.IMAGE.elements.presentation).checked;\n const imageAltError = alt === '' && !presentation;\n this.toggleVisibility(Selectors.IMAGE.elements.altWarning, imageAltError);\n this.toggleAriaInvalid([Selectors.IMAGE.elements.alt, Selectors.IMAGE.elements.presentation], 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) => element.setAttribute('aria-invalid', predicate));\n });\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\n const constrain = this.form.querySelector(Selectors.IMAGE.elements.constrain).value;\n if (constrain) {\n classList.push(Selectors.IMAGE.styles.responsive);\n }\n\n return {\n url: this.form.querySelector(Selectors.IMAGE.elements.url).value,\n alt: this.form.querySelector(Selectors.IMAGE.elements.alt).value,\n width: this.form.querySelector(Selectors.IMAGE.elements.width).value,\n height: this.form.querySelector(Selectors.IMAGE.elements.height).value,\n presentation: this.form.querySelector(Selectors.IMAGE.elements.presentation).checked,\n customStyle: this.form.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.form.querySelector(Selectors.IMAGE.elements.url).value;\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.form.querySelector(Selectors.IMAGE.elements.width).value;\n if (!this.isPercentageValue(width) && isNaN(parseInt(width, 10))) {\n this.form.querySelector(Selectors.IMAGE.elements.width).focus();\n pendingPromise.resolve();\n return;\n }\n\n const height = this.form.querySelector(Selectors.IMAGE.elements.height).value;\n if (!this.isPercentageValue(height) && isNaN(parseInt(height, 10))) {\n this.form.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();\n }\n\n handleKeyupCharacterCount() {\n const alt = this.form.querySelector(Selectors.IMAGE.elements.alt).value;\n const current = this.form.querySelector('#currentcount');\n current.innerHTML = alt.length;\n }\n\n autoAdjustSize(forceHeight = false) {\n // If we do not know the image size, do not do anything.\n if (!this.rawImageDimensions) {\n return;\n }\n\n const widthField = this.form.querySelector(Selectors.IMAGE.elements.width);\n const heightField = this.form.querySelector(Selectors.IMAGE.elements.height);\n const normalizeFieldData = (fieldData) => {\n fieldData.isPercentageValue = !!this.isPercentageValue(fieldData.field.value);\n if (fieldData.isPercentageValue) {\n fieldData.percentValue = parseInt(fieldData.field.value, 10);\n fieldData.pixelSize = this.rawImageDimensions[fieldData.type] / 100 * fieldData.percentValue;\n } else {\n fieldData.pixelSize = parseInt(fieldData.field.value, 10);\n fieldData.percentValue = fieldData.pixelSize / this.rawImageDimensions[fieldData.type] * 100;\n }\n\n return fieldData;\n };\n\n const getKeyField = () => {\n const getValue = () => {\n if (forceHeight) {\n return {\n field: heightField,\n type: 'height',\n };\n } else {\n return {\n field: widthField,\n type: 'width',\n };\n }\n };\n\n const currentValue = getValue();\n if (currentValue.field.value === '') {\n currentValue.field.value = this.rawImageDimensions[currentValue.type];\n }\n\n return normalizeFieldData(currentValue);\n };\n\n const getRelativeField = () => {\n if (forceHeight) {\n return normalizeFieldData({\n field: widthField,\n type: 'width',\n });\n } else {\n return normalizeFieldData({\n field: heightField,\n type: 'height',\n });\n }\n };\n\n\n const setImageDimensions = (image, keyField, relativeField, forceHeight = false) => {\n const getStyleValue = (field) => field.isPercentageValue ? `${field.percentValue}%` : `${field.pixelSize}px`;\n\n // If the values are constrained, then only update the width.\n if (forceHeight) {\n if (keyField.type === 'width') {\n image.style.width = getStyleValue(keyField);\n } else {\n image.style.width = getStyleValue(relativeField);\n }\n } else {\n image.style[keyField.type] = getStyleValue(keyField);\n image.style[relativeField.type] = getStyleValue(relativeField);\n }\n };\n\n const imagePreview = this.form.querySelector(Selectors.IMAGE.elements.preview);\n // Clear the existing preview sizes.\n imagePreview.style.width = '';\n imagePreview.style.height = '';\n\n // Now update with the new values.\n const constrainField = this.form.querySelector(Selectors.IMAGE.elements.constrain);\n const keyField = getKeyField();\n const relativeField = getRelativeField();\n if (constrainField.checked) {\n // We are keeping the image in proportion.\n // Calculate the size for the relative field.\n if (keyField.isPercentageValue) {\n // In proportion, so the percentages are the same.\n relativeField.field.value = keyField.field.value;\n relativeField.percentValue = keyField.percentValue;\n } else {\n relativeField.pixelSize = Math.round(\n keyField.pixelSize / this.rawImageDimensions[keyField.type] * this.rawImageDimensions[relativeField.type]\n );\n relativeField.field.value = relativeField.pixelSize;\n }\n }\n setImageDimensions(imagePreview, keyField, relativeField, !!constrainField.checked);\n }\n\n getSelectedImageProperties() {\n const image = this.getSelectedImage();\n if (!image) {\n this.selectedImage = null;\n return null;\n }\n\n const properties = {\n src: null,\n alt: null,\n width: null,\n height: null,\n presentation: false,\n };\n\n const getImageHeight = (image) => {\n if (!this.isPercentageValue(String(image.height))) {\n return parseInt(image.height, 10);\n }\n\n return image.height;\n };\n\n const getImageWidth = (image) => {\n if (!this.isPercentageValue(String(image.width))) {\n return parseInt(image.width, 10);\n }\n\n return image.width;\n };\n\n // Get the current selection.\n this.selectedImage = image;\n\n properties.customStyle = image.style.cssText;\n\n const width = getImageWidth(image);\n if (width !== 0) {\n properties.width = width;\n }\n\n const height = getImageHeight(image);\n if (height !== 0) {\n properties.height = height;\n }\n\n properties.src = image.getAttribute('src');\n properties.alt = image.getAttribute('alt') || '';\n properties.presentation = (image.getAttribute('role') === 'presentation');\n\n return properties;\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\n if (imgElm && (imgElm.nodeName.toUpperCase() !== 'IMG' || this.isPlaceholderImage(imgElm))) {\n return null;\n }\n return imgElm;\n }\n\n isPlaceholderImage(imgElm) {\n if (imgElm.nodeName.toUpperCase() !== 'IMG') {\n return false;\n }\n\n return (imgElm.hasAttribute('data-mce-object') || imgElm.hasAttribute('data-mce-placeholder'));\n }\n\n isPercentageValue(value) {\n return value.match(/\\d+%/);\n }\n\n async registerEventListeners() {\n await this.currentModal.getBody();\n const root = this.currentModal.getRoot()[0];\n\n this.form = root.querySelector(Selectors.IMAGE.elements.form);\n root.addEventListener('click', (e) => {\n const submitAction = e.target.closest(Selectors.IMAGE.actions.submit);\n const imageBrowserAction = e.target.closest(Selectors.IMAGE.actions.imageBrowser);\n if (submitAction) {\n e.preventDefault();\n this.setImage();\n }\n if (imageBrowserAction && this.canShowFilePicker) {\n e.preventDefault();\n displayFilepicker(this.editor, 'image').then((params) => {\n const self = this;\n this.filePickerCallback(params, self);\n\n return;\n }).catch();\n }\n });\n\n root.addEventListener('change', (e) => {\n const urlEle = e.target.closest(Selectors.IMAGE.elements.url);\n if (urlEle) {\n this.hasErrorUrlField();\n }\n\n const presentationEle = e.target.closest(Selectors.IMAGE.elements.presentation);\n if (presentationEle) {\n this.hasErrorAltField();\n }\n\n const constrainEle = e.target.closest(Selectors.IMAGE.elements.constrain);\n if (constrainEle) {\n this.autoAdjustSize(true);\n }\n });\n\n root.addEventListener('blur', (e) => {\n if (e.target.nodeType === Node.ELEMENT_NODE) {\n const urlEle = e.target.closest(Selectors.IMAGE.elements.url);\n if (urlEle) {\n this.urlChanged();\n }\n\n const altEle = e.target.closest(Selectors.IMAGE.elements.alt);\n if (altEle) {\n this.hasErrorAltField();\n }\n\n const widthEle = e.target.closest(Selectors.IMAGE.elements.width);\n if (widthEle) {\n this.autoAdjustSize();\n }\n\n const heightEle = e.target.closest(Selectors.IMAGE.elements.height);\n if (heightEle) {\n this.autoAdjustSize(true);\n }\n }\n }, true);\n\n // Character count.\n root.addEventListener('keyup', (e) => {\n const altEle = e.target.closest(Selectors.IMAGE.elements.alt);\n if (altEle) {\n this.handleKeyupCharacterCount();\n }\n });\n }\n}\n"],"names":["constructor","editor","WIDTH","HEIGHT","permissions","canShowFilePicker","filepicker","rawImageDimensions","currentImageData","this","getCurrentImageData","currentModal","ImageModal","create","title","templateContext","getTemplateContext","src","loadPreviewImage","registerEventListeners","data","elementid","id","showfilepicker","selectedImageProperties","getSelectedImageProperties","properties","haspreview","alt","presentation","filePickerCallback","params","self","url","form","querySelector","Selectors","IMAGE","elements","value","width","height","storeImageDimensions","image","DEFAULTS","currentWidth","element","getCurrentWidth","currentHeight","getCurrentHeight","preview","setAttribute","style","display","constrain","isPercentageValue","checked","disabled","widthRatio","Math","round","parseInt","heightRatio","Image","addEventListener","autoAdjustSize","urlChanged","input","hasErrorUrlField","urlError","toggleVisibility","urlWarning","toggleAriaInvalid","hasErrorAltField","imageAltError","altWarning","selector","predicate","querySelectorAll","forEach","selectors","updateWarning","getImageContext","classList","push","styles","responsive","customStyle","classlist","join","setImage","pendingPromise","Pending","resolve","isNaN","focus","render","then","html","insertContent","destroy","catch","handleKeyupCharacterCount","innerHTML","length","forceHeight","widthField","heightField","normalizeFieldData","fieldData","field","percentValue","pixelSize","type","imagePreview","constrainField","keyField","currentValue","getKeyField","relativeField","getStyleValue","setImageDimensions","getSelectedImage","selectedImage","cssText","String","getImageWidth","getImageHeight","getAttribute","imgElm","selection","getNode","figureElm","dom","getParent","select","nodeName","toUpperCase","isPlaceholderImage","hasAttribute","match","getBody","root","getRoot","e","submitAction","target","closest","actions","submit","imageBrowserAction","imageBrowser","preventDefault","nodeType","Node","ELEMENT_NODE"],"mappings":"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"} \ No newline at end of file +{"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 ImageModal from './imagemodal';\nimport {getImagePermissions} from './options';\nimport {getFilePicker} from 'editor_tiny/options';\nimport {ImageInsert} from 'tiny_media/imageinsert';\nimport {\n bodyImageInsert,\n footerImageInsert,\n} from 'tiny_media/imagehelpers';\n\nexport default class MediaImage {\n canShowFilePicker = false;\n editor = null;\n currentModal = null;\n /**\n * @type {HTMLElement|null} The root element.\n */\n root = null;\n\n constructor(editor) {\n const permissions = getImagePermissions(editor);\n const options = getFilePicker(editor, 'image');\n // Indicates whether the file picker can be shown.\n this.canShowFilePicker = permissions.filepicker\n && (typeof options !== 'undefined')\n && Object.keys(options.repositories).length > 0;\n // Indicates whether the drop zone area can be shown.\n this.canShowDropZone = Object.values(options.repositories).some(repository => repository.type === 'upload');\n\n this.editor = editor;\n }\n\n async displayDialogue() {\n this.currentModal = await ImageModal.create();\n this.root = this.currentModal.getRoot()[0];\n this.loadInsertImage();\n }\n\n /**\n * Displays an insert image view asynchronously.\n *\n * @returns {Promise}\n */\n loadInsertImage = async function() {\n const templateContext = {\n elementid: this.editor.id,\n showfilepicker: this.canShowFilePicker,\n showdropzone: this.canShowDropZone,\n };\n\n Promise.all([bodyImageInsert(templateContext, this.root), footerImageInsert(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"],"names":["constructor","editor","async","templateContext","elementid","this","id","showfilepicker","canShowFilePicker","showdropzone","canShowDropZone","Promise","all","root","then","ImageInsert","currentModal","init","catch","error","window","console","log","permissions","options","filepicker","Object","keys","repositories","length","values","some","repository","type","ImageModal","create","getRoot","loadInsertImage"],"mappings":"kjBAyCIA,YAAYC,kDARQ,iCACX,0CACM,kCAIR,8CA0BWC,uBACRC,gBAAkB,CACpBC,UAAWC,KAAKJ,OAAOK,GACvBC,eAAgBF,KAAKG,kBACrBC,aAAcJ,KAAKK,iBAGvBC,QAAQC,IAAI,EAAC,iCAAgBT,gBAAiBE,KAAKQ,OAAO,mCAAkBV,gBAAiBE,KAAKQ,QAC7FC,MAAK,KACkB,IAAIC,yBACpBV,KAAKQ,KACLR,KAAKJ,OACLI,KAAKW,aACLX,KAAKG,kBACLH,KAAKK,iBAEGO,UAGfC,OAAMC,QACHC,OAAOC,QAAQC,IAAIH,mBA3CrBI,aAAc,gCAAoBtB,QAClCuB,SAAU,2BAAcvB,OAAQ,cAEjCO,kBAAoBe,YAAYE,iBACV,IAAZD,SACRE,OAAOC,KAAKH,QAAQI,cAAcC,OAAS,OAE7CnB,gBAAkBgB,OAAOI,OAAON,QAAQI,cAAcG,MAAKC,YAAkC,WAApBA,WAAWC,YAEpFhC,OAASA,oCAITe,mBAAqBkB,oBAAWC,cAChCtB,KAAOR,KAAKW,aAAaoB,UAAU,QACnCC"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/media/amd/build/imagehelpers.min.js b/lib/editor/tiny/plugins/media/amd/build/imagehelpers.min.js new file mode 100644 index 00000000000..f139395658d --- /dev/null +++ b/lib/editor/tiny/plugins/media/amd/build/imagehelpers.min.js @@ -0,0 +1,10 @@ +define("tiny_media/imagehelpers",["exports","core/templates"],(function(_exports,_templates){var obj; +/** + * Tiny media plugin image helpers. + * + * @module tiny_media/imagehelpers + * @copyright 2024 Meirza + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.footerImageInsert=_exports.bodyImageInsert=void 0,_templates=(obj=_templates)&&obj.__esModule?obj:{default:obj};_exports.bodyImageInsert=async(templateContext,root)=>_templates.default.renderForPromise("tiny_media/insert_image_modal_insert",{...templateContext}).then((_ref=>{let{html:html,js:js}=_ref;_templates.default.replaceNodeContents(root.querySelector(".tiny_image_body_template"),html,js)})).catch((error=>{window.console.log(error)}));_exports.footerImageInsert=async(templateContext,root)=>_templates.default.renderForPromise("tiny_media/insert_image_modal_insert_footer",{...templateContext}).then((_ref2=>{let{html:html,js:js}=_ref2;_templates.default.replaceNodeContents(root.querySelector(".tiny_image_footer_template"),html,js)})).catch((error=>{window.console.log(error)}))})); + +//# sourceMappingURL=imagehelpers.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/media/amd/build/imagehelpers.min.js.map b/lib/editor/tiny/plugins/media/amd/build/imagehelpers.min.js.map new file mode 100644 index 00000000000..1ee2bff303a --- /dev/null +++ b/lib/editor/tiny/plugins/media/amd/build/imagehelpers.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"imagehelpers.min.js","sources":["../src/imagehelpers.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 helpers.\n *\n * @module tiny_media/imagehelpers\n * @copyright 2024 Meirza \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Templates from 'core/templates';\n\n/**\n * Renders and inserts the body template for inserting an image into the modal.\n *\n * @param {object} templateContext - The context for rendering the template.\n * @param {HTMLElement} root - The root element where the template will be inserted.\n * @returns {Promise}\n */\nexport const bodyImageInsert = async(templateContext, root) => {\n return Templates.renderForPromise('tiny_media/insert_image_modal_insert', {...templateContext})\n .then(({html, js}) => {\n Templates.replaceNodeContents(root.querySelector('.tiny_image_body_template'), html, js);\n return;\n })\n .catch(error => {\n window.console.log(error);\n });\n};\n\n/**\n * Renders and inserts the footer template for inserting an image into the modal.\n *\n * @param {object} templateContext - The context for rendering the template.\n * @param {HTMLElement} root - The root element where the template will be inserted.\n * @returns {Promise}\n */\nexport const footerImageInsert = async(templateContext, root) => {\n return Templates.renderForPromise('tiny_media/insert_image_modal_insert_footer', {...templateContext})\n .then(({html, js}) => {\n Templates.replaceNodeContents(root.querySelector('.tiny_image_footer_template'), html, js);\n return;\n })\n .catch(error => {\n window.console.log(error);\n });\n};\n"],"names":["async","templateContext","root","Templates","renderForPromise","then","_ref","html","js","replaceNodeContents","querySelector","catch","error","window","console","log","_ref2"],"mappings":";;;;;;;+MAgC+BA,MAAMC,gBAAiBC,OAC3CC,mBAAUC,iBAAiB,uCAAwC,IAAIH,kBAC7EI,MAAKC,WAACC,KAACA,KAADC,GAAOA,4BACAC,oBAAoBP,KAAKQ,cAAc,6BAA8BH,KAAMC,OAGxFG,OAAMC,QACHC,OAAOC,QAAQC,IAAIH,qCAWMZ,MAAMC,gBAAiBC,OAC7CC,mBAAUC,iBAAiB,8CAA+C,IAAIH,kBACpFI,MAAKW,YAACT,KAACA,KAADC,GAAOA,6BACAC,oBAAoBP,KAAKQ,cAAc,+BAAgCH,KAAMC,OAG1FG,OAAMC,QACHC,OAAOC,QAAQC,IAAIH"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/media/amd/build/imageinsert.min.js b/lib/editor/tiny/plugins/media/amd/build/imageinsert.min.js new file mode 100644 index 00000000000..d0315dcd499 --- /dev/null +++ b/lib/editor/tiny/plugins/media/amd/build/imageinsert.min.js @@ -0,0 +1,3 @@ +define("tiny_media/imageinsert",["exports","core/prefetch","core/str","./common"],(function(_exports,_prefetch,_str,_common){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.ImageInsert=void 0,(0,_prefetch.prefetchStrings)("tiny_media",["insertimage","enterurl","enterurlor","imageurlrequired","uploading","loading"]);_exports.ImageInsert=class{constructor(root,editor,currentModal,canShowFilePicker,canShowDropZone){var obj,key,value;value=async function(){const langStringKeys=["insertimage","enterurl","enterurlor","imageurlrequired","uploading","loading"],langStringvalues=await(0,_str.getStrings)([...langStringKeys].map((key=>({key:key,component:_common.component}))));this.langStrings=Object.fromEntries(langStringKeys.map(((key,index)=>[key,langStringvalues[index]]))),this.currentModal.setTitle(this.langStrings.insertimage)},(key="init")in(obj=this)?Object.defineProperty(obj,key,{value:value,enumerable:!0,configurable:!0,writable:!0}):obj[key]=value,this.root=root,this.editor=editor,this.currentModal=currentModal,this.canShowFilePicker=canShowFilePicker,this.canShowDropZone=canShowDropZone}}})); + +//# sourceMappingURL=imageinsert.min.js.map \ No newline at end of file diff --git a/lib/editor/tiny/plugins/media/amd/build/imageinsert.min.js.map b/lib/editor/tiny/plugins/media/amd/build/imageinsert.min.js.map new file mode 100644 index 00000000000..b548e43aedb --- /dev/null +++ b/lib/editor/tiny/plugins/media/amd/build/imageinsert.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"imageinsert.min.js","sources":["../src/imageinsert.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 insertion class for Moodle.\n *\n * @module tiny_media/imageinsert\n * @copyright 2024 Meirza \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport {prefetchStrings} from 'core/prefetch';\nimport {getStrings} from 'core/str';\nimport {component} from \"./common\";\n\nprefetchStrings('tiny_media', [\n 'insertimage',\n 'enterurl',\n 'enterurlor',\n 'imageurlrequired',\n 'uploading',\n 'loading',\n]);\n\nexport class ImageInsert {\n\n constructor(\n root,\n editor,\n currentModal,\n canShowFilePicker,\n canShowDropZone,\n ) {\n this.root = root;\n this.editor = editor;\n this.currentModal = currentModal;\n this.canShowFilePicker = canShowFilePicker;\n this.canShowDropZone = canShowDropZone;\n }\n\n init = async function() {\n // Get the localization lang strings and turn them into object.\n const langStringKeys = [\n 'insertimage',\n 'enterurl',\n 'enterurlor',\n 'imageurlrequired',\n 'uploading',\n 'loading',\n ];\n const langStringvalues = await getStrings([...langStringKeys].map((key) => ({key, component})));\n\n // Convert array to object.\n this.langStrings = Object.fromEntries(langStringKeys.map((key, index) => [key, langStringvalues[index]]));\n this.currentModal.setTitle(this.langStrings.insertimage);\n };\n}"],"names":["constructor","root","editor","currentModal","canShowFilePicker","canShowDropZone","async","langStringKeys","langStringvalues","map","key","component","langStrings","Object","fromEntries","index","setTitle","this","insertimage"],"mappings":"+OA2BgB,aAAc,CAC1B,cACA,WACA,aACA,mBACA,YACA,uCAKAA,YACIC,KACAC,OACAC,aACAC,kBACAC,yCASGC,uBAEGC,eAAiB,CACnB,cACA,WACA,aACA,mBACA,YACA,WAEEC,uBAAyB,mBAAW,IAAID,gBAAgBE,KAAKC,OAAUA,IAAAA,IAAKC,UAAAA,4BAG7EC,YAAcC,OAAOC,YAAYP,eAAeE,KAAI,CAACC,IAAKK,QAAU,CAACL,IAAKF,iBAAiBO,gBAC3FZ,aAAaa,SAASC,KAAKL,YAAYM,kJArBvCjB,KAAOA,UACPC,OAASA,YACTC,aAAeA,kBACfC,kBAAoBA,uBACpBC,gBAAkBA"} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/media/amd/src/image.js b/lib/editor/tiny/plugins/media/amd/src/image.js index 19c4f04f132..82a752f136e 100644 --- a/lib/editor/tiny/plugins/media/amd/src/image.js +++ b/lib/editor/tiny/plugins/media/amd/src/image.js @@ -21,521 +21,69 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -import Templates from 'core/templates'; -import {getString} from 'core/str'; -import Pending from 'core/pending'; -import {displayFilepicker} from 'editor_tiny/utils'; -import Selectors from './selectors'; import ImageModal from './imagemodal'; import {getImagePermissions} from './options'; import {getFilePicker} from 'editor_tiny/options'; +import {ImageInsert} from 'tiny_media/imageinsert'; +import { + bodyImageInsert, + footerImageInsert, +} from 'tiny_media/imagehelpers'; export default class MediaImage { - DEFAULTS = { - WIDTH: 160, - HEIGHT: 160, - }; - - form = null; - rawImageDimensions = null; canShowFilePicker = false; editor = null; currentModal = null; - selectedImage = null; + /** + * @type {HTMLElement|null} The root element. + */ + root = null; constructor(editor) { const permissions = getImagePermissions(editor); - + const options = getFilePicker(editor, 'image'); // Indicates whether the file picker can be shown. - this.canShowFilePicker = permissions.filepicker && (typeof getFilePicker(editor, 'image') !== 'undefined'); + this.canShowFilePicker = permissions.filepicker + && (typeof options !== 'undefined') + && Object.keys(options.repositories).length > 0; + // Indicates whether the drop zone area can be shown. + this.canShowDropZone = Object.values(options.repositories).some(repository => repository.type === 'upload'); this.editor = editor; } async displayDialogue() { - // Reset the image dimensions. - this.rawImageDimensions = null; - - const currentImageData = await this.getCurrentImageData(); - this.currentModal = await ImageModal.create({ - title: getString('imageproperties', 'tiny_media'), - templateContext: await this.getTemplateContext(currentImageData), - }); - - if (currentImageData && currentImageData.src) { - this.loadPreviewImage(currentImageData.src); - } - - await this.registerEventListeners(); + this.currentModal = await ImageModal.create(); + this.root = this.currentModal.getRoot()[0]; + this.loadInsertImage(); } - async getTemplateContext(data) { - return { + /** + * Displays an insert image view asynchronously. + * + * @returns {Promise} + */ + loadInsertImage = async function() { + const templateContext = { elementid: this.editor.id, showfilepicker: this.canShowFilePicker, - ...data, - }; - } - - async getCurrentImageData() { - const selectedImageProperties = this.getSelectedImageProperties(); - if (!selectedImageProperties) { - return {}; - } - - const properties = {...selectedImageProperties}; - - if (properties.src) { - properties.haspreview = true; - } - - if (!properties.alt) { - properties.presentation = true; - } - - return properties; - } - - filePickerCallback(params, self) { - if (params.url) { - const input = self.form.querySelector(Selectors.IMAGE.elements.url); - input.value = params.url; - - // Auto set the width and height. - self.form.querySelector(Selectors.IMAGE.elements.width).value = ''; - self.form.querySelector(Selectors.IMAGE.elements.height).value = ''; - - // Load the preview image. - self.loadPreviewImage(params.url); - } - } - - storeImageDimensions(image) { - // Store dimensions of the raw image, falling back to defaults for images without dimensions (e.g. SVG). - this.rawImageDimensions = { - width: image.width || this.DEFAULTS.WIDTH, - height: image.height || this.DEFAULTS.HEIGHT, + showdropzone: this.canShowDropZone, }; - const getCurrentWidth = (element) => { - if (element.value === '') { - element.value = this.rawImageDimensions.width; - } - return element.value; - }; - const getCurrentHeight = (element) => { - if (element.value === '') { - element.value = this.rawImageDimensions.height; - } - return element.value; - }; - - const widthInput = this.form.querySelector(Selectors.IMAGE.elements.width); - const currentWidth = getCurrentWidth(widthInput); - - const heightInput = this.form.querySelector(Selectors.IMAGE.elements.height); - const currentHeight = getCurrentHeight(heightInput); - - const preview = this.form.querySelector(Selectors.IMAGE.elements.preview); - preview.setAttribute('src', image.src); - preview.style.display = 'inline'; - - const constrain = this.form.querySelector(Selectors.IMAGE.elements.constrain); - if (this.isPercentageValue(currentWidth) && this.isPercentageValue(currentHeight)) { - constrain.checked = currentWidth === currentHeight; - } else if (image.width === 0 || image.height === 0) { - // If we don't have both dimensions of the image, we can't auto-size it, so disable control. - constrain.disabled = 'disabled'; - } else { - // This is the same as comparing to 3 decimal places. - const widthRatio = Math.round(1000 * parseInt(currentWidth, 10) / image.width); - const heightRatio = Math.round(1000 * parseInt(currentHeight, 10) / image.height); - constrain.checked = widthRatio === heightRatio; - } - } - - loadPreviewImage(url) { - const image = new Image(); - - image.addEventListener('error', () => { - const preview = this.form.querySelector(Selectors.IMAGE.elements.preview); - preview.style.display = 'none'; - }); - image.addEventListener('load', () => { - this.storeImageDimensions(image); - this.autoAdjustSize(); - }); - - image.src = url; - } - - urlChanged() { - const input = this.form.querySelector(Selectors.IMAGE.elements.url); - - if (input.value) { - // Load the preview image. - this.loadPreviewImage(input.value); - } - } - - hasErrorUrlField() { - const url = this.form.querySelector(Selectors.IMAGE.elements.url).value; - const urlError = url === ''; - this.toggleVisibility(Selectors.IMAGE.elements.urlWarning, urlError); - this.toggleAriaInvalid([Selectors.IMAGE.elements.url], urlError); - - return urlError; - } - - hasErrorAltField() { - const alt = this.form.querySelector(Selectors.IMAGE.elements.alt).value; - const presentation = this.form.querySelector(Selectors.IMAGE.elements.presentation).checked; - const imageAltError = alt === '' && !presentation; - this.toggleVisibility(Selectors.IMAGE.elements.altWarning, imageAltError); - this.toggleAriaInvalid([Selectors.IMAGE.elements.alt, Selectors.IMAGE.elements.presentation], 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)); - }); - } - - updateWarning() { - const urlError = this.hasErrorUrlField(); - const imageAltError = this.hasErrorAltField(); - - return urlError || imageAltError; - } - - getImageContext() { - // Check if there are any accessibility issues. - if (this.updateWarning()) { - return null; - } - - const classList = []; - - const constrain = this.form.querySelector(Selectors.IMAGE.elements.constrain).value; - if (constrain) { - classList.push(Selectors.IMAGE.styles.responsive); - } - - return { - url: this.form.querySelector(Selectors.IMAGE.elements.url).value, - alt: this.form.querySelector(Selectors.IMAGE.elements.alt).value, - width: this.form.querySelector(Selectors.IMAGE.elements.width).value, - height: this.form.querySelector(Selectors.IMAGE.elements.height).value, - presentation: this.form.querySelector(Selectors.IMAGE.elements.presentation).checked, - customStyle: this.form.querySelector(Selectors.IMAGE.elements.customStyle).value, - classlist: classList.join(' '), - }; - } - - setImage() { - const pendingPromise = new Pending('tiny_media:setImage'); - const url = this.form.querySelector(Selectors.IMAGE.elements.url).value; - if (url === '') { - return; - } - - // Check if there are any accessibility issues. - if (this.updateWarning()) { - pendingPromise.resolve(); - return; - } - - // Check for invalid width or height. - const width = this.form.querySelector(Selectors.IMAGE.elements.width).value; - if (!this.isPercentageValue(width) && isNaN(parseInt(width, 10))) { - this.form.querySelector(Selectors.IMAGE.elements.width).focus(); - pendingPromise.resolve(); - return; - } - - const height = this.form.querySelector(Selectors.IMAGE.elements.height).value; - if (!this.isPercentageValue(height) && isNaN(parseInt(height, 10))) { - this.form.querySelector(Selectors.IMAGE.elements.height).focus(); - pendingPromise.resolve(); - return; - } - - Templates.render('tiny_media/image', this.getImageContext()) - .then((html) => { - this.editor.insertContent(html); - this.currentModal.destroy(); - pendingPromise.resolve(); - - return html; - }) - .catch(); - } - - handleKeyupCharacterCount() { - const alt = this.form.querySelector(Selectors.IMAGE.elements.alt).value; - const current = this.form.querySelector('#currentcount'); - current.innerHTML = alt.length; - } - - autoAdjustSize(forceHeight = false) { - // If we do not know the image size, do not do anything. - if (!this.rawImageDimensions) { - return; - } - - const widthField = this.form.querySelector(Selectors.IMAGE.elements.width); - const heightField = this.form.querySelector(Selectors.IMAGE.elements.height); - const normalizeFieldData = (fieldData) => { - fieldData.isPercentageValue = !!this.isPercentageValue(fieldData.field.value); - if (fieldData.isPercentageValue) { - fieldData.percentValue = parseInt(fieldData.field.value, 10); - fieldData.pixelSize = this.rawImageDimensions[fieldData.type] / 100 * fieldData.percentValue; - } else { - fieldData.pixelSize = parseInt(fieldData.field.value, 10); - fieldData.percentValue = fieldData.pixelSize / this.rawImageDimensions[fieldData.type] * 100; - } - - return fieldData; - }; - - const getKeyField = () => { - const getValue = () => { - if (forceHeight) { - return { - field: heightField, - type: 'height', - }; - } else { - return { - field: widthField, - type: 'width', - }; - } - }; - - const currentValue = getValue(); - if (currentValue.field.value === '') { - currentValue.field.value = this.rawImageDimensions[currentValue.type]; - } - - return normalizeFieldData(currentValue); - }; - - const getRelativeField = () => { - if (forceHeight) { - return normalizeFieldData({ - field: widthField, - type: 'width', - }); - } else { - return normalizeFieldData({ - field: heightField, - type: 'height', - }); - } - }; - - - const setImageDimensions = (image, keyField, relativeField, forceHeight = false) => { - const getStyleValue = (field) => field.isPercentageValue ? `${field.percentValue}%` : `${field.pixelSize}px`; - - // If the values are constrained, then only update the width. - if (forceHeight) { - if (keyField.type === 'width') { - image.style.width = getStyleValue(keyField); - } else { - image.style.width = getStyleValue(relativeField); - } - } else { - image.style[keyField.type] = getStyleValue(keyField); - image.style[relativeField.type] = getStyleValue(relativeField); - } - }; - - const imagePreview = this.form.querySelector(Selectors.IMAGE.elements.preview); - // Clear the existing preview sizes. - imagePreview.style.width = ''; - imagePreview.style.height = ''; - - // Now update with the new values. - const constrainField = this.form.querySelector(Selectors.IMAGE.elements.constrain); - const keyField = getKeyField(); - const relativeField = getRelativeField(); - if (constrainField.checked) { - // We are keeping the image in proportion. - // Calculate the size for the relative field. - if (keyField.isPercentageValue) { - // In proportion, so the percentages are the same. - relativeField.field.value = keyField.field.value; - relativeField.percentValue = keyField.percentValue; - } else { - relativeField.pixelSize = Math.round( - keyField.pixelSize / this.rawImageDimensions[keyField.type] * this.rawImageDimensions[relativeField.type] + Promise.all([bodyImageInsert(templateContext, this.root), footerImageInsert(templateContext, this.root)]) + .then(() => { + const imageinsert = new ImageInsert( + this.root, + this.editor, + this.currentModal, + this.canShowFilePicker, + this.canShowDropZone, ); - relativeField.field.value = relativeField.pixelSize; - } - } - setImageDimensions(imagePreview, keyField, relativeField, !!constrainField.checked); - } - - getSelectedImageProperties() { - const image = this.getSelectedImage(); - if (!image) { - this.selectedImage = null; - return null; - } - - const properties = { - src: null, - alt: null, - width: null, - height: null, - presentation: false, - }; - - const getImageHeight = (image) => { - if (!this.isPercentageValue(String(image.height))) { - return parseInt(image.height, 10); - } - - return image.height; - }; - - const getImageWidth = (image) => { - if (!this.isPercentageValue(String(image.width))) { - return parseInt(image.width, 10); - } - - return image.width; - }; - - // Get the current selection. - this.selectedImage = image; - - properties.customStyle = image.style.cssText; - - const width = getImageWidth(image); - if (width !== 0) { - properties.width = width; - } - - const height = getImageHeight(image); - if (height !== 0) { - properties.height = height; - } - - properties.src = image.getAttribute('src'); - properties.alt = image.getAttribute('alt') || ''; - properties.presentation = (image.getAttribute('role') === 'presentation'); - - return properties; - } - - getSelectedImage() { - const imgElm = this.editor.selection.getNode(); - const figureElm = this.editor.dom.getParent(imgElm, 'figure.image'); - if (figureElm) { - return this.editor.dom.select('img', figureElm)[0]; - } - - if (imgElm && (imgElm.nodeName.toUpperCase() !== 'IMG' || this.isPlaceholderImage(imgElm))) { - return null; - } - return imgElm; - } - - isPlaceholderImage(imgElm) { - if (imgElm.nodeName.toUpperCase() !== 'IMG') { - return false; - } - - return (imgElm.hasAttribute('data-mce-object') || imgElm.hasAttribute('data-mce-placeholder')); - } - - isPercentageValue(value) { - return value.match(/\d+%/); - } - - async registerEventListeners() { - await this.currentModal.getBody(); - const root = this.currentModal.getRoot()[0]; - - this.form = root.querySelector(Selectors.IMAGE.elements.form); - root.addEventListener('click', (e) => { - const submitAction = e.target.closest(Selectors.IMAGE.actions.submit); - const imageBrowserAction = e.target.closest(Selectors.IMAGE.actions.imageBrowser); - if (submitAction) { - e.preventDefault(); - this.setImage(); - } - if (imageBrowserAction && this.canShowFilePicker) { - e.preventDefault(); - displayFilepicker(this.editor, 'image').then((params) => { - const self = this; - this.filePickerCallback(params, self); - - return; - }).catch(); - } - }); - - root.addEventListener('change', (e) => { - const urlEle = e.target.closest(Selectors.IMAGE.elements.url); - if (urlEle) { - this.hasErrorUrlField(); - } - - const presentationEle = e.target.closest(Selectors.IMAGE.elements.presentation); - if (presentationEle) { - this.hasErrorAltField(); - } - - const constrainEle = e.target.closest(Selectors.IMAGE.elements.constrain); - if (constrainEle) { - this.autoAdjustSize(true); - } - }); - - root.addEventListener('blur', (e) => { - if (e.target.nodeType === Node.ELEMENT_NODE) { - const urlEle = e.target.closest(Selectors.IMAGE.elements.url); - if (urlEle) { - this.urlChanged(); - } - - const altEle = e.target.closest(Selectors.IMAGE.elements.alt); - if (altEle) { - this.hasErrorAltField(); - } - - const widthEle = e.target.closest(Selectors.IMAGE.elements.width); - if (widthEle) { - this.autoAdjustSize(); - } - - const heightEle = e.target.closest(Selectors.IMAGE.elements.height); - if (heightEle) { - this.autoAdjustSize(true); - } - } - }, true); - - // Character count. - root.addEventListener('keyup', (e) => { - const altEle = e.target.closest(Selectors.IMAGE.elements.alt); - if (altEle) { - this.handleKeyupCharacterCount(); - } - }); - } + imageinsert.init(); + return; + }) + .catch(error => { + window.console.log(error); + }); + }; } diff --git a/lib/editor/tiny/plugins/media/amd/src/imagehelpers.js b/lib/editor/tiny/plugins/media/amd/src/imagehelpers.js new file mode 100644 index 00000000000..c857254df38 --- /dev/null +++ b/lib/editor/tiny/plugins/media/amd/src/imagehelpers.js @@ -0,0 +1,60 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Tiny media plugin image helpers. + * + * @module tiny_media/imagehelpers + * @copyright 2024 Meirza + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Templates from 'core/templates'; + +/** + * Renders and inserts the body template for inserting an image into the modal. + * + * @param {object} templateContext - The context for rendering the template. + * @param {HTMLElement} root - The root element where the template will be inserted. + * @returns {Promise} + */ +export const bodyImageInsert = async(templateContext, root) => { + return Templates.renderForPromise('tiny_media/insert_image_modal_insert', {...templateContext}) + .then(({html, js}) => { + Templates.replaceNodeContents(root.querySelector('.tiny_image_body_template'), html, js); + return; + }) + .catch(error => { + window.console.log(error); + }); +}; + +/** + * Renders and inserts the footer template for inserting an image into the modal. + * + * @param {object} templateContext - The context for rendering the template. + * @param {HTMLElement} root - The root element where the template will be inserted. + * @returns {Promise} + */ +export const footerImageInsert = async(templateContext, root) => { + return Templates.renderForPromise('tiny_media/insert_image_modal_insert_footer', {...templateContext}) + .then(({html, js}) => { + Templates.replaceNodeContents(root.querySelector('.tiny_image_footer_template'), html, js); + return; + }) + .catch(error => { + window.console.log(error); + }); +}; diff --git a/lib/editor/tiny/plugins/media/amd/src/imageinsert.js b/lib/editor/tiny/plugins/media/amd/src/imageinsert.js new file mode 100644 index 00000000000..9f83418bf9a --- /dev/null +++ b/lib/editor/tiny/plugins/media/amd/src/imageinsert.js @@ -0,0 +1,69 @@ +// 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 insertion class for Moodle. + * + * @module tiny_media/imageinsert + * @copyright 2024 Meirza + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import {prefetchStrings} from 'core/prefetch'; +import {getStrings} from 'core/str'; +import {component} from "./common"; + +prefetchStrings('tiny_media', [ + 'insertimage', + 'enterurl', + 'enterurlor', + 'imageurlrequired', + 'uploading', + 'loading', +]); + +export class ImageInsert { + + constructor( + root, + editor, + currentModal, + canShowFilePicker, + canShowDropZone, + ) { + this.root = root; + this.editor = editor; + this.currentModal = currentModal; + this.canShowFilePicker = canShowFilePicker; + this.canShowDropZone = canShowDropZone; + } + + init = async function() { + // Get the localization lang strings and turn them into object. + const langStringKeys = [ + 'insertimage', + 'enterurl', + 'enterurlor', + 'imageurlrequired', + 'uploading', + 'loading', + ]; + const langStringvalues = await getStrings([...langStringKeys].map((key) => ({key, component}))); + + // Convert array to object. + this.langStrings = Object.fromEntries(langStringKeys.map((key, index) => [key, langStringvalues[index]])); + this.currentModal.setTitle(this.langStrings.insertimage); + }; +} \ No newline at end of file diff --git a/lib/editor/tiny/plugins/media/lang/en/tiny_media.php b/lib/editor/tiny/plugins/media/lang/en/tiny_media.php index 41a5ab112fb..a6444d70049 100644 --- a/lib/editor/tiny/plugins/media/lang/en/tiny_media.php +++ b/lib/editor/tiny/plugins/media/lang/en/tiny_media.php @@ -29,11 +29,13 @@ $string['addmetadatatrack'] = 'Add metadata track'; $string['addsource_help'] = 'You are recommended to provide an alternative media source, as desktop and mobile browsers support different file formats.'; $string['addsource'] = 'Add alternative source'; $string['addsubtitlestrack'] = 'Add subtitle track'; +$string['addurl'] = 'Add'; $string['advancedsettings'] = 'Advanced settings'; $string['audio'] = 'Audio'; $string['audiosourcelabel'] = 'Audio source URL'; $string['autoplay'] = 'Play automatically'; $string['browserepositories'] = 'Browse repositories...'; +$string['browserepositoriesimage'] = 'Browse repositories'; $string['captions_help'] = 'Captions may be used to describe everything happening in the track, including non-verbal sounds such as a phone ringing.'; $string['captions'] = 'Captions'; $string['captionssourcelabel'] = 'Caption track URL'; @@ -53,7 +55,8 @@ $string['enteralt'] = 'Describe this image for someone who cannot see it'; $string['entername'] = 'Name'; $string['entersource'] = 'Source URL'; $string['entertitle'] = 'Title'; -$string['enterurl'] = 'URL'; +$string['enterurl'] = 'Add via URL'; +$string['enterurlor'] = 'Or add via URL'; $string['filemanager'] = 'File manager'; $string['hasmissingfiles'] = 'Warning! The following files that are referenced in the text area appear to be missing:'; $string['height'] = 'Height'; @@ -61,6 +64,7 @@ $string['helplinktext'] = 'Media helper'; $string['imagebuttontitle'] = 'Image'; $string['imageproperties'] = 'Image properties'; $string['imageurlrequired'] = 'An image must have a URL.'; +$string['insertimage'] = 'Insert image'; $string['label'] = 'Label'; $string['languagesavailable'] = 'Languages available'; $string['languagesinstalled'] = 'Languages installed'; diff --git a/lib/editor/tiny/plugins/media/styles.css b/lib/editor/tiny/plugins/media/styles.css index 0975c6714c6..daf191aa802 100644 --- a/lib/editor/tiny/plugins/media/styles.css +++ b/lib/editor/tiny/plugins/media/styles.css @@ -33,3 +33,11 @@ iframe.mm_iframe { font-weight: 600; color: red; } + +.tiny_image_form .tiny_image_dropzone { + padding: 1.5rem 1rem; +} + +.tiny_image_form .tiny_image_loader_container { + height: 200px; +} diff --git a/lib/editor/tiny/plugins/media/templates/insert_image_modal.mustache b/lib/editor/tiny/plugins/media/templates/insert_image_modal.mustache index 8245810621b..cc87296e6f9 100644 --- a/lib/editor/tiny/plugins/media/templates/insert_image_modal.mustache +++ b/lib/editor/tiny/plugins/media/templates/insert_image_modal.mustache @@ -21,92 +21,41 @@ Example context (json): { + "uniqid": 0, "elementid": "exampleId", - "src": "https://moodle.org/logo.png", - "alt": "Image description", - "presentation": true, - "width": 600, - "height": 400, - "customStyle": "", - "haspreview": true + "loading": {}, + "title": "Insert image" } }} {{< core/modal }} {{$body}}
-