Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

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