MDL-78096 tiny_media: Add a new insert image

This commit is contained in:
meirzamoodle
2024-04-04 09:32:35 +07:00
parent ef5969e794
commit de026219d3
14 changed files with 329 additions and 565 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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 <meirza.arson@moodle.com>
* @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
@@ -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 <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny media plugin image helpers.\n *\n * @module tiny_media/imagehelpers\n * @copyright 2024 Meirza <[email protected]>\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<void>}\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<void>}\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"}
@@ -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
@@ -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 <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny media plugin image insertion class for Moodle.\n *\n * @module tiny_media/imageinsert\n * @copyright 2024 Meirza <[email protected]>\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"}
+41 -493
View File
@@ -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<void>}
*/
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);
});
};
}
@@ -0,0 +1,60 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Tiny media plugin image helpers.
*
* @module tiny_media/imagehelpers
* @copyright 2024 Meirza <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
import Templates from 'core/templates';
/**
* Renders and inserts the body template for inserting an image into the modal.
*
* @param {object} templateContext - The context for rendering the template.
* @param {HTMLElement} root - The root element where the template will be inserted.
* @returns {Promise<void>}
*/
export const bodyImageInsert = async(templateContext, root) => {
return Templates.renderForPromise('tiny_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<void>}
*/
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);
});
};
@@ -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 <http://www.gnu.org/licenses/>.
/**
* Tiny media plugin image insertion class for Moodle.
*
* @module tiny_media/imageinsert
* @copyright 2024 Meirza <[email protected]>
* @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);
};
}
@@ -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';
+8
View File
@@ -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;
}
@@ -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}}
<form class="tiny_image_form">
<div style="display:none" role="alert" class="alert alert-warning mb-1 tiny_image_urlwarning">
<label for="{{elementid}}_tiny_image_urlentry">
<!-- URL warning -->
<div role="alert" class="d-none alert alert-warning mb-1 tiny_image_urlwarning">
<label>
{{#str}} imageurlrequired, tiny_media {{/str}}
</label>
</div>
{{#showfilepicker}}
<div class="mb-1">
<label for="{{elementid}}_tiny_image_urlentry">{{#str}} enterurl, tiny_media {{/str}}</label>
<div class="input-group input-append w-100">
<input name=urlentry class="form-control tiny_image_urlentry" type="url" id="{{elementid}}_tiny_image_urlentry" size="32" value="{{src}}"/>
<span class="input-group-append">
<button class="btn btn-secondary openimagebrowser" type="button">
{{#str}} browserepositories, tiny_media {{/str}}
</button>
</span>
</div>
</div>
{{/showfilepicker}}
{{^showfilepicker}}
<div class="mb-1">
<label for="{{elementid}}_tiny_image_urlentry">{{#str}} enterurl, tiny_media {{/str}}</label>
<input name="urlentry" class="form-control fullwidth tiny_image_urlentry" type="url" id="{{elementid}}_tiny_image_urlentry" size="32" value="{{src}}">
</div>
{{/showfilepicker}}
<div style="display:none" role="alert" class="alert alert-warning mb-1 tiny_image_altwarning">
<label for="{{elementid}}_tiny_image_altentry">
<!-- Presentation warning -->
<div role="alert" class="d-none tiny_image_altwarning alert alert-warning mb-1">
<label>
{{#str}} presentationoraltrequired, tiny_media {{/str}}
</label>
</div>
<div class="mb-1">
<label for="{{elementid}}_tiny_image_altentry">{{#str}} enteralt, tiny_media {{/str}}</label>
<textarea class="form-control fullwidth tiny_image_altentry" id="{{elementid}}_tiny_image_altentry" name="altentry" maxlength="125">{{alt}}</textarea>
<div id="the-count" class="d-flex justify-content-end small">
<span id="currentcount">0</span>
<span id="maximumcount"> / 125</span>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input tiny_image_presentation" id="{{elementid}}_tiny_image_presentation" {{# presentation }}checked{{/ presentation }}/>
<label class="form-check-label" for="{{elementid}}_tiny_image_presentation">{{#str}} presentation, tiny_media {{/str}}</label>
</div>
</div>
<div class="mb-1">
<label class="" for="{{elementid}}_tiny_image_size">{{#str}} size, tiny_media {{/str}}</label>
<div id="{{elementid}}_tiny_image_size" class="d-flex flex-wrap align-items-center tiny_image_size">
<label class="accesshide" for="{{elementid}}_tiny_image_widthentry">{{#str}} width, tiny_media {{/str}}</label>
<input type="text" class="form-control mr-1 w-auto input-mini tiny_image_widthentry" id="{{elementid}}_tiny_image_widthentry" size="4" value="{{width}}"/>
x
<label class="accesshide" for="{{elementid}}_tiny_image_heightentry">{{#str}} height, tiny_media {{/str}}</label>
<input type="text" class="form-control ml-1 w-auto input-mini tiny_image_heightentry" id="{{elementid}}_tiny_image_heightentry" size="4" value="{{height}}"/>
<div class="form-check ml-2">
<input type="checkbox" class="form-check-input tiny_image_constrain" id="{{elementid}}_tiny_image_constrain"/>
<label class="form-check-label" for="{{elementid}}_tiny_image_constrain">{{#str}} constrain, tiny_media {{/str}}</label>
</div>
</div>
</div>
<input type="hidden" class="tiny_image_customstyle" value="{{customStyle}}"/>
<br/>
<div class="mdl-align">
<div class="tiny_image_preview_box">
<img class="tiny_image_preview" alt=""
{{#haspreview}} src="{{src}}" {{/haspreview}}
{{^haspreview}} style="display: none;" {{/haspreview}}
/>
<!-- Preloader icon -->
<div role="progressbar" class="d-none tiny_image_loader lead border rounded text-center">
<div class="tiny_image_loader_container d-flex flex-column justify-content-center">
<span data-region="loading-icon-container">
{{> core/loading }}
</span>
<div>{{#str}} loading, tiny_media {{/str}}</div>
</div>
</div>
<div class="tiny_image_body_template"></div>
</form>
{{/body}}
{{$footer}}
<button class="btn btn-secondary tiny_image_urlentrysubmit" type="submit">{{#str}} saveimage, tiny_media {{/str}}</button>
<button type="button" class="btn btn-secondary" data-action="cancel">{{#str}} cancel, moodle {{/str}}</button>
<div class="tiny_image_footer_template container"></div>
{{/footer}}
{{/ core/modal }}
@@ -0,0 +1,54 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template tiny_media/insert_image_modal_insert
Insert image body template.
Example context (json):
{
"showdropzone": true,
"showfilepicker": true
}
}}
<div class="tiny_image_insert_image">
<div class="tiny_image_dropzone mb-1 d-flex align-items-center flex-column justify-content-center border rounded">
{{#showdropzone}}
<div class="tiny_image_dropzone_iconlabel text-center" role="button">
<!-- Dropzone icon -->
<i class="tiny_image_dropzone_icon fa-6x pb-2 text-secondary fa fa-cloud-upload"></i>
<!-- Dropzone string -->
<div class="tiny_image_dropzone_label lead text-center">
<p class="mb-0">{{#str}} draganddrop, tiny_media {{/str}}</p>
</div>
</div>
<!-- File input -->
<input type="file" id="tiny_image_fileinput" accept="image/*" class="d-none">
{{/showdropzone}}
{{^showfilepicker}}
<div class="tiny_image_dropzone_iconlabel text-center">
<!-- Dropzone icon -->
<i class="tiny_image_dropzone_icon fa-6x pb-2 text-secondary fa fa-cloud"></i>
<!-- Dropzone string -->
<div class="tiny_image_dropzone_label lead text-center">
<p class="mb-0">{{#str}} repositorynotpermitted, tiny_media {{/str}}</p>
</div>
</div>
{{/showfilepicker}}
</div>
</div>
@@ -0,0 +1,57 @@
{{!
This file is part of Moodle - http://moodle.org/
Moodle is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Moodle is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Moodle. If not, see <http://www.gnu.org/licenses/>.
}}
{{!
@template tiny_media/insert_image_modal_insert_footer
Insert image footer template.
Example context (json):
{
"showdropzone": true,
"elementid": "exampleId",
"src": "https://moodle.org/logo.png",
"showfilepicker": true
}
}}
<div class="row">
<!-- First Column -->
<div class="col-md-6 d-flex align-items-center p-0">
<!-- Row 1: URL related label -->
{{#showdropzone}}
<label for="{{elementid}}_tiny_image_urlentry" title="{{#str}} enterurlor, tiny_media {{/str}}" class="tiny_image_url_label mr-1">{{#str}} enterurlor, tiny_media {{/str}}</label>
{{/showdropzone}}
{{^showdropzone}}
<label for="{{elementid}}_tiny_image_urlentry" title="{{#str}} enterurl, tiny_media {{/str}}" class="tiny_image_url_label mr-1">{{#str}} enterurl, tiny_media {{/str}}</label>
{{/showdropzone}}
<label title="{{#str}} filename, tiny_media {{/str}}" class="d-none tiny_image_filename text-truncate mr-1">{{#str}} filename, tiny_media {{/str}}</label>
<!-- Row 2: URL entry input. Needed by the insert image step and image details step if the image URL source from external -->
<input name="urlentry" class="tiny_image_urlentry form-control w-50 mr-1" type="url" id="{{elementid}}_tiny_image_urlentry" size="32" value="{{src}}">
<!-- Row 3: Add button. Needed by the insert image step -->
<button disabled class="tiny_image_addurl btn btn-secondary mr-1" type="button">{{#str}} addurl, tiny_media {{/str}}</button>
</div>
<!-- Column 2: Saving, canceling, browsing repositories buttons -->
<div class="col-md-6 text-right mt-2 md-0 p-0">
<!-- Row 1: Cancel button -->
<button type="button" class="btn btn-secondary" data-action="cancel">{{#str}} cancel, moodle {{/str}}</button>
{{#showfilepicker}}
<!-- Row 3: Browse repositories button -->
<button class="openimagebrowser btn btn-secondary" type="button">{{#str}} browserepositoriesimage, tiny_media {{/str}}</button>
{{/showfilepicker}}
</div>
</div>