lib/editor/tiny/plugins/media/amd/src/image.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 class for Moodle.
  17. *
  18. * @module tiny_media/image
  19. * @copyright 2022 Huong Nguyen <huongnv13@gmail.com>
  20. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  21. */
  22. import Selectors from './selectors';
  23. import ImageModal from './imagemodal';
  24. import {getImagePermissions} from './options';
  25. import {getFilePicker} from 'editor_tiny/options';
  26. import {ImageInsert} from 'tiny_media/imageinsert';
  27. import {ImageDetails} from 'tiny_media/imagedetails';
  28. import {prefetchStrings} from 'core/prefetch';
  29. import {getString} from 'core/str';
  30. import {
  31. body,
  32. footer,
  33. hideElements,
  34. showElements,
  35. isPercentageValue,
  36. } from './helpers';
  37. import {MAX_LENGTH_ALT} from './imagehelpers';
  38. prefetchStrings('tiny_media', [
  39. 'imageurlrequired',
  40. 'sizecustom_help',
  41. ]);
  42. export default class MediaImage {
  43. canShowFilePicker = false;
  44. editor = null;
  45. currentModal = null;
  46. /**
  47. * @type {HTMLElement|null} The root element.
  48. */
  49. root = null;
  50. constructor(editor) {
  51. const permissions = getImagePermissions(editor);
  52. const options = getFilePicker(editor, 'image');
  53. // Indicates whether the file picker can be shown.
  54. this.canShowFilePicker = permissions.filepicker
  55. && (typeof options !== 'undefined')
  56. && Object.keys(options.repositories).length > 0;
  57. // Indicates whether the drop zone area can be shown.
  58. this.canShowDropZone = (typeof options !== 'undefined') &&
  59. Object.values(options.repositories).some(repository => repository.type === 'upload');
  60. this.editor = editor;
  61. }
  62. async displayDialogue() {
  63. const currentImageData = await this.getCurrentImageData();
  64. this.currentModal = await ImageModal.create();
  65. this.root = this.currentModal.getRoot()[0];
  66. if (currentImageData && currentImageData.src) {
  67. this.loadPreviewImage(currentImageData.src);
  68. } else {
  69. this.loadInsertImage();
  70. }
  71. }
  72. /**
  73. * Displays an insert image view asynchronously.
  74. *
  75. * @returns {Promise<void>}
  76. */
  77. loadInsertImage = async function() {
  78. const templateContext = {
  79. elementid: this.editor.id,
  80. showfilepicker: this.canShowFilePicker,
  81. showdropzone: this.canShowDropZone,
  82. bodyTemplate: Selectors.IMAGE.template.body.insertImageBody,
  83. footerTemplate: Selectors.IMAGE.template.footer.insertImageFooter,
  84. selector: Selectors.IMAGE.type,
  85. };
  86. Promise.all([body(templateContext, this.root), footer(templateContext, this.root)])
  87. .then(() => {
  88. const imageinsert = new ImageInsert(
  89. this.root,
  90. this.editor,
  91. this.currentModal,
  92. this.canShowFilePicker,
  93. this.canShowDropZone,
  94. );
  95. imageinsert.init();
  96. return;
  97. })
  98. .catch(error => {
  99. window.console.log(error);
  100. });
  101. };
  102. async getTemplateContext(data) {
  103. return {
  104. elementid: this.editor.id,
  105. showfilepicker: this.canShowFilePicker,
  106. ...data,
  107. };
  108. }
  109. async getCurrentImageData() {
  110. const selectedImageProperties = this.getSelectedImageProperties();
  111. if (!selectedImageProperties) {
  112. return {};
  113. }
  114. const properties = {...selectedImageProperties};
  115. if (properties.src) {
  116. properties.haspreview = true;
  117. }
  118. if (!properties.alt) {
  119. properties.presentation = true;
  120. }
  121. return properties;
  122. }
  123. /**
  124. * Asynchronously loads and previews an image from the provided URL.
  125. *
  126. * @param {string} url - The URL of the image to load and preview.
  127. * @returns {Promise<void>}
  128. */
  129. loadPreviewImage = async function(url) {
  130. this.startImageLoading();
  131. const image = new Image();
  132. image.src = url;
  133. image.addEventListener('error', async() => {
  134. const urlWarningLabelEle = this.root.querySelector(Selectors.IMAGE.elements.urlWarning);
  135. urlWarningLabelEle.innerHTML = await getString('imageurlrequired', 'tiny_media');
  136. showElements(Selectors.IMAGE.elements.urlWarning, this.root);
  137. this.stopImageLoading();
  138. });
  139. image.addEventListener('load', async() => {
  140. const currentImageData = await this.getCurrentImageData();
  141. let templateContext = await this.getTemplateContext(currentImageData);
  142. templateContext.sizecustomhelpicon = {text: await getString('sizecustom_help', 'tiny_media')};
  143. templateContext.bodyTemplate = Selectors.IMAGE.template.body.insertImageDetailsBody;
  144. templateContext.footerTemplate = Selectors.IMAGE.template.footer.insertImageDetailsFooter;
  145. templateContext.selector = Selectors.IMAGE.type;
  146. templateContext.maxlengthalt = MAX_LENGTH_ALT;
  147. Promise.all([body(templateContext, this.root), footer(templateContext, this.root)])
  148. .then(() => {
  149. this.stopImageLoading();
  150. return;
  151. })
  152. .then(() => {
  153. const imagedetails = new ImageDetails(
  154. this.root,
  155. this.editor,
  156. this.currentModal,
  157. this.canShowFilePicker,
  158. this.canShowDropZone,
  159. url,
  160. image,
  161. );
  162. imagedetails.init();
  163. return;
  164. })
  165. .catch(error => {
  166. window.console.log(error);
  167. });
  168. });
  169. };
  170. getSelectedImageProperties() {
  171. const image = this.getSelectedImage();
  172. if (!image) {
  173. this.selectedImage = null;
  174. return null;
  175. }
  176. const properties = {
  177. src: null,
  178. alt: null,
  179. width: null,
  180. height: null,
  181. presentation: false,
  182. customStyle: '', // Custom CSS styles applied to the image.
  183. };
  184. const getImageHeight = (image) => {
  185. if (!isPercentageValue(String(image.height))) {
  186. return parseInt(image.height, 10);
  187. }
  188. return image.height;
  189. };
  190. const getImageWidth = (image) => {
  191. if (!isPercentageValue(String(image.width))) {
  192. return parseInt(image.width, 10);
  193. }
  194. return image.width;
  195. };
  196. // Get the current selection.
  197. this.selectedImage = image;
  198. properties.customStyle = image.style.cssText;
  199. const width = getImageWidth(image);
  200. if (width !== 0) {
  201. properties.width = width;
  202. }
  203. const height = getImageHeight(image);
  204. if (height !== 0) {
  205. properties.height = height;
  206. }
  207. properties.src = image.getAttribute('src');
  208. properties.alt = image.getAttribute('alt') || '';
  209. properties.presentation = (image.getAttribute('role') === 'presentation');
  210. return properties;
  211. }
  212. getSelectedImage() {
  213. const imgElm = this.editor.selection.getNode();
  214. const figureElm = this.editor.dom.getParent(imgElm, 'figure.image');
  215. if (figureElm) {
  216. return this.editor.dom.select('img', figureElm)[0];
  217. }
  218. if (imgElm && (imgElm.nodeName.toUpperCase() !== 'IMG' || this.isPlaceholderImage(imgElm))) {
  219. return null;
  220. }
  221. return imgElm;
  222. }
  223. isPlaceholderImage(imgElm) {
  224. if (imgElm.nodeName.toUpperCase() !== 'IMG') {
  225. return false;
  226. }
  227. return (imgElm.hasAttribute('data-mce-object') || imgElm.hasAttribute('data-mce-placeholder'));
  228. }
  229. /**
  230. * Displays the upload loader and disables UI elements while loading a file.
  231. */
  232. startImageLoading() {
  233. showElements(Selectors.IMAGE.elements.loaderIcon, this.root);
  234. hideElements(Selectors.IMAGE.elements.insertImage, this.root);
  235. }
  236. /**
  237. * Displays the upload loader and disables UI elements while loading a file.
  238. */
  239. stopImageLoading() {
  240. hideElements(Selectors.IMAGE.elements.loaderIcon, this.root);
  241. showElements(Selectors.IMAGE.elements.insertImage, this.root);
  242. }
  243. }