lib/editor/tiny/plugins/media/amd/src/imageinsert.js

  1. // This file is part of Moodle - http://moodle.org/
  2. //
  3. // Moodle is free software: you can redistribute it and/or modify
  4. // it under the terms of the GNU General Public License as published by
  5. // the Free Software Foundation, either version 3 of the License, or
  6. // (at your option) any later version.
  7. //
  8. // Moodle is distributed in the hope that it will be useful,
  9. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. // GNU General Public License for more details.
  12. //
  13. // You should have received a copy of the GNU General Public License
  14. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  15. /**
  16. * Tiny media plugin image insertion class for Moodle.
  17. *
  18. * @module tiny_media/imageinsert
  19. * @copyright 2024 Meirza <meirza.arson@moodle.com>
  20. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  21. */
  22. import Selectors from './selectors';
  23. import Dropzone from 'core/dropzone';
  24. import uploadFile from 'editor_tiny/uploader';
  25. import {prefetchStrings} from 'core/prefetch';
  26. import {getStrings} from 'core/str';
  27. import {component} from "./common";
  28. import {getFilePicker} from 'editor_tiny/options';
  29. import {displayFilepicker} from 'editor_tiny/utils';
  30. import {ImageDetails} from 'tiny_media/imagedetails';
  31. import {
  32. body,
  33. footer,
  34. hideElements,
  35. showElements,
  36. isValidUrl,
  37. } from './helpers';
  38. import {MAX_LENGTH_ALT} from './imagehelpers';
  39. prefetchStrings('tiny_media', [
  40. 'insertimage',
  41. 'enterurl',
  42. 'enterurlor',
  43. 'imageurlrequired',
  44. 'uploading',
  45. 'loading',
  46. 'addfilesdrop',
  47. 'sizecustom_help',
  48. ]);
  49. export class ImageInsert {
  50. constructor(
  51. root,
  52. editor,
  53. currentModal,
  54. canShowFilePicker,
  55. canShowDropZone,
  56. ) {
  57. this.root = root;
  58. this.editor = editor;
  59. this.currentModal = currentModal;
  60. this.canShowFilePicker = canShowFilePicker;
  61. this.canShowDropZone = canShowDropZone;
  62. }
  63. init = async function() {
  64. // Get the localization lang strings and turn them into object.
  65. const langStringKeys = [
  66. 'insertimage',
  67. 'enterurl',
  68. 'enterurlor',
  69. 'imageurlrequired',
  70. 'uploading',
  71. 'loading',
  72. 'addfilesdrop',
  73. 'sizecustom_help',
  74. ];
  75. const langStringvalues = await getStrings([...langStringKeys].map((key) => ({key, component})));
  76. // Convert array to object.
  77. this.langStrings = Object.fromEntries(langStringKeys.map((key, index) => [key, langStringvalues[index]]));
  78. this.currentModal.setTitle(this.langStrings.insertimage);
  79. if (this.canShowDropZone) {
  80. const dropZoneEle = document.querySelector(Selectors.IMAGE.elements.dropzoneContainer);
  81. // Accepted types can be either a string or an array.
  82. let acceptedTypes = getFilePicker(this.editor, 'image').accepted_types;
  83. if (Array.isArray(acceptedTypes)) {
  84. acceptedTypes = acceptedTypes.join(',');
  85. }
  86. const dropZone = new Dropzone(
  87. dropZoneEle,
  88. acceptedTypes,
  89. files => {
  90. this.handleUploadedFile(files);
  91. }
  92. );
  93. dropZone.setLabel(this.langStrings.addfilesdrop);
  94. dropZone.init();
  95. }
  96. await this.registerEventListeners();
  97. };
  98. /**
  99. * Enables or disables the URL-related buttons in the footer based on the current URL and input value.
  100. */
  101. toggleUrlButton() {
  102. const urlInput = this.root.querySelector(Selectors.IMAGE.elements.url);
  103. const url = urlInput.value;
  104. const addUrl = this.root.querySelector(Selectors.IMAGE.actions.addUrl);
  105. addUrl.disabled = !(url !== "" && isValidUrl(url));
  106. }
  107. /**
  108. * Handles changes in the image URL input field and loads a preview of the image if the URL has changed.
  109. */
  110. urlChanged() {
  111. hideElements(Selectors.IMAGE.elements.urlWarning, this.root);
  112. const input = this.root.querySelector(Selectors.IMAGE.elements.url);
  113. if (input.value && input.value !== this.currentUrl) {
  114. this.loadPreviewImage(input.value);
  115. }
  116. }
  117. /**
  118. * Loads and displays a preview image based on the provided URL, and handles image loading events.
  119. *
  120. * @param {string} url - The URL of the image to load and display.
  121. */
  122. loadPreviewImage = function(url) {
  123. this.startImageLoading();
  124. this.currentUrl = url;
  125. const image = new Image();
  126. image.src = url;
  127. image.addEventListener('error', () => {
  128. const urlWarningLabelEle = this.root.querySelector(Selectors.IMAGE.elements.urlWarning);
  129. urlWarningLabelEle.innerHTML = this.langStrings.imageurlrequired;
  130. showElements(Selectors.IMAGE.elements.urlWarning, this.root);
  131. this.currentUrl = "";
  132. this.stopImageLoading();
  133. });
  134. image.addEventListener('load', () => {
  135. let templateContext = {};
  136. templateContext.sizecustomhelpicon = {text: this.langStrings.sizecustom_help};
  137. templateContext.bodyTemplate = Selectors.IMAGE.template.body.insertImageDetailsBody;
  138. templateContext.footerTemplate = Selectors.IMAGE.template.footer.insertImageDetailsFooter;
  139. templateContext.selector = Selectors.IMAGE.type;
  140. templateContext.maxlengthalt = MAX_LENGTH_ALT;
  141. Promise.all([body(templateContext, this.root), footer(templateContext, this.root)])
  142. .then(() => {
  143. const imagedetails = new ImageDetails(
  144. this.root,
  145. this.editor,
  146. this.currentModal,
  147. this.canShowFilePicker,
  148. this.canShowDropZone,
  149. this.currentUrl,
  150. image,
  151. );
  152. imagedetails.init();
  153. return;
  154. }).then(() => {
  155. this.stopImageLoading();
  156. return;
  157. })
  158. .catch(error => {
  159. window.console.log(error);
  160. });
  161. });
  162. };
  163. /**
  164. * Displays the upload loader and disables UI elements while loading a file.
  165. */
  166. startImageLoading() {
  167. showElements(Selectors.IMAGE.elements.loaderIcon, this.root);
  168. const elementsToHide = [
  169. Selectors.IMAGE.elements.insertImage,
  170. Selectors.IMAGE.elements.urlWarning,
  171. Selectors.IMAGE.elements.modalFooter,
  172. ];
  173. hideElements(elementsToHide, this.root);
  174. }
  175. /**
  176. * Displays the upload loader and disables UI elements while loading a file.
  177. */
  178. stopImageLoading() {
  179. hideElements(Selectors.IMAGE.elements.loaderIcon, this.root);
  180. const elementsToShow = [
  181. Selectors.IMAGE.elements.insertImage,
  182. Selectors.IMAGE.elements.modalFooter,
  183. ];
  184. showElements(elementsToShow, this.root);
  185. }
  186. filePickerCallback(params) {
  187. if (params.url) {
  188. this.loadPreviewImage(params.url);
  189. }
  190. }
  191. /**
  192. * Updates the content of the loader icon.
  193. *
  194. * @param {HTMLElement} root - The root element containing the loader icon.
  195. * @param {object} langStrings - An object containing language strings.
  196. * @param {number|null} progress - The progress percentage (optional).
  197. * @returns {void}
  198. */
  199. updateLoaderIcon = (root, langStrings, progress = null) => {
  200. const loaderIcon = root.querySelector(Selectors.IMAGE.elements.loaderIconContainer + ' div');
  201. loaderIcon.innerHTML = progress !== null ? `${langStrings.uploading} ${Math.round(progress)}%` : langStrings.loading;
  202. };
  203. /**
  204. * Handles the uploaded file, initiates the upload process, and updates the UI during the upload.
  205. *
  206. * @param {FileList} files - The list of files to upload (usually from a file input field).
  207. * @returns {Promise<void>} A promise that resolves when the file is uploaded and processed.
  208. */
  209. handleUploadedFile = async(files) => {
  210. try {
  211. this.startImageLoading();
  212. const fileURL = await uploadFile(this.editor, 'image', files[0], files[0].name, (progress) => {
  213. this.updateLoaderIcon(this.root, this.langStrings, progress);
  214. });
  215. // Set the loader icon content to "loading" after the file upload completes.
  216. this.updateLoaderIcon(this.root, this.langStrings);
  217. this.filePickerCallback({url: fileURL});
  218. } catch (error) {
  219. // Handle the error.
  220. const urlWarningLabelEle = this.root.querySelector(Selectors.IMAGE.elements.urlWarning);
  221. urlWarningLabelEle.innerHTML = error.error !== undefined ? error.error : error;
  222. showElements(Selectors.IMAGE.elements.urlWarning, this.root);
  223. this.stopImageLoading();
  224. }
  225. };
  226. registerEventListeners() {
  227. this.root.addEventListener('click', async(e) => {
  228. const addUrlEle = e.target.closest(Selectors.IMAGE.actions.addUrl);
  229. if (addUrlEle) {
  230. this.urlChanged();
  231. }
  232. const imageBrowserAction = e.target.closest(Selectors.IMAGE.actions.imageBrowser);
  233. if (imageBrowserAction && this.canShowFilePicker) {
  234. e.preventDefault();
  235. const params = await displayFilepicker(this.editor, 'image');
  236. this.filePickerCallback(params);
  237. }
  238. });
  239. this.root.addEventListener('input', (e) => {
  240. const urlEle = e.target.closest(Selectors.IMAGE.elements.url);
  241. if (urlEle) {
  242. this.toggleUrlButton();
  243. }
  244. });
  245. const fileInput = this.root.querySelector(Selectors.IMAGE.elements.fileInput);
  246. if (fileInput) {
  247. fileInput.addEventListener('change', () => {
  248. this.handleUploadedFile(fileInput.files);
  249. });
  250. }
  251. }
  252. }