Proyectos de Subversion Moodle

Rev

| 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";
30
import {displayFilepicker} from 'editor_tiny/utils';
31
import {ImageDetails} from 'tiny_media/imagedetails';
32
import {
33
    showElements,
34
    hideElements,
35
    bodyImageDetails,
36
    footerImageDetails,
37
} from 'tiny_media/imagehelpers';
38
 
39
prefetchStrings('tiny_media', [
40
    'insertimage',
41
    'enterurl',
42
    'enterurlor',
43
    'imageurlrequired',
44
    'uploading',
45
    'loading',
46
    'addfilesdrop',
47
    'sizecustom_help',
48
]);
49
 
50
export class ImageInsert {
51
 
52
    constructor(
53
        root,
54
        editor,
55
        currentModal,
56
        canShowFilePicker,
57
        canShowDropZone,
58
    ) {
59
        this.root = root;
60
        this.editor = editor;
61
        this.currentModal = currentModal;
62
        this.canShowFilePicker = canShowFilePicker;
63
        this.canShowDropZone = canShowDropZone;
64
    }
65
 
66
    init = async function() {
67
        // Get the localization lang strings and turn them into object.
68
        const langStringKeys = [
69
            'insertimage',
70
            'enterurl',
71
            'enterurlor',
72
            'imageurlrequired',
73
            'uploading',
74
            'loading',
75
            'addfilesdrop',
76
            'sizecustom_help',
77
        ];
78
        const langStringvalues = await getStrings([...langStringKeys].map((key) => ({key, component})));
79
 
80
        // Convert array to object.
81
        this.langStrings = Object.fromEntries(langStringKeys.map((key, index) => [key, langStringvalues[index]]));
82
        this.currentModal.setTitle(this.langStrings.insertimage);
83
        if (this.canShowDropZone) {
84
            const dropZoneEle = document.querySelector(Selectors.IMAGE.elements.dropzoneContainer);
85
            const dropZone = new Dropzone(
86
                dropZoneEle,
87
                'image/*',
88
                files => {
89
                    this.handleUploadedFile(files);
90
                }
91
            );
92
            dropZone.setLabel(this.langStrings.addfilesdrop);
93
            dropZone.init();
94
        }
95
        await this.registerEventListeners();
96
    };
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 !== "" && this.isValidUrl(url));
106
    }
107
 
108
    /**
109
     * Check if given string is a valid URL.
110
     *
111
     * @param {String} urlString URL the link will point to.
112
     * @returns {boolean} True is valid, otherwise false.
113
     */
114
    isValidUrl = urlString => {
115
        const urlPattern = new RegExp('^(https?:\\/\\/)?' + // Protocol.
116
                                    '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // Domain name.
117
                                    '((\\d{1,3}\\.){3}\\d{1,3})|localhost)' + // OR ip (v4) address, localhost.
118
                                    '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'); // Port and path.
119
        return !!urlPattern.test(urlString);
120
    };
121
 
122
    /**
123
     * Handles changes in the image URL input field and loads a preview of the image if the URL has changed.
124
     */
125
    urlChanged() {
126
        hideElements(Selectors.IMAGE.elements.urlWarning, this.root);
127
        const input = this.root.querySelector(Selectors.IMAGE.elements.url);
128
        if (input.value && input.value !== this.currentUrl) {
129
            this.loadPreviewImage(input.value);
130
        }
131
    }
132
 
133
    /**
134
     * Loads and displays a preview image based on the provided URL, and handles image loading events.
135
     *
136
     * @param {string} url - The URL of the image to load and display.
137
     */
138
    loadPreviewImage = function(url) {
139
        this.startImageLoading();
140
        this.currentUrl = url;
141
        const image = new Image();
142
        image.src = url;
143
        image.addEventListener('error', () => {
144
            const urlWarningLabelEle = this.root.querySelector(Selectors.IMAGE.elements.urlWarning);
145
            urlWarningLabelEle.innerHTML = this.langStrings.imageurlrequired;
146
            showElements(Selectors.IMAGE.elements.urlWarning, this.root);
147
            this.currentUrl = "";
148
            this.stopImageLoading();
149
        });
150
 
151
        image.addEventListener('load', () => {
152
            let templateContext = {};
153
            templateContext.sizecustomhelpicon = {text: this.langStrings.sizecustom_help};
154
            Promise.all([bodyImageDetails(templateContext, this.root), footerImageDetails(templateContext, this.root)])
155
                .then(() => {
156
                    const imagedetails = new ImageDetails(
157
                        this.root,
158
                        this.editor,
159
                        this.currentModal,
160
                        this.canShowFilePicker,
161
                        this.canShowDropZone,
162
                        this.currentUrl,
163
                        image,
164
                    );
165
                    imagedetails.init();
166
                    return;
167
                }).then(() => {
168
                    this.stopImageLoading();
169
                    return;
170
                })
171
                .catch(error => {
172
                    window.console.log(error);
173
                });
174
        });
175
    };
176
 
177
    /**
178
     * Displays the upload loader and disables UI elements while loading a file.
179
     */
180
    startImageLoading() {
181
        showElements(Selectors.IMAGE.elements.loaderIcon, this.root);
182
        const elementsToHide = [
183
            Selectors.IMAGE.elements.insertImage,
184
            Selectors.IMAGE.elements.urlWarning,
185
            Selectors.IMAGE.elements.modalFooter,
186
        ];
187
        hideElements(elementsToHide, this.root);
188
    }
189
 
190
    /**
191
     * Displays the upload loader and disables UI elements while loading a file.
192
     */
193
    stopImageLoading() {
194
        hideElements(Selectors.IMAGE.elements.loaderIcon, this.root);
195
        const elementsToShow = [
196
            Selectors.IMAGE.elements.insertImage,
197
            Selectors.IMAGE.elements.modalFooter,
198
        ];
199
        showElements(elementsToShow, this.root);
200
    }
201
 
202
    filePickerCallback(params) {
203
        if (params.url) {
204
            this.loadPreviewImage(params.url);
205
        }
206
    }
207
 
208
    /**
209
     * Updates the content of the loader icon.
210
     *
211
     * @param {HTMLElement} root - The root element containing the loader icon.
212
     * @param {object} langStrings - An object containing language strings.
213
     * @param {number|null} progress - The progress percentage (optional).
214
     * @returns {void}
215
     */
216
    updateLoaderIcon = (root, langStrings, progress = null) => {
217
        const loaderIcon = root.querySelector(Selectors.IMAGE.elements.loaderIconContainer + ' div');
218
        loaderIcon.innerHTML = progress !== null ? `${langStrings.uploading} ${Math.round(progress)}%` : langStrings.loading;
219
    };
220
 
221
    /**
222
     * Handles the uploaded file, initiates the upload process, and updates the UI during the upload.
223
     *
224
     * @param {FileList} files - The list of files to upload (usually from a file input field).
225
     * @returns {Promise<void>} A promise that resolves when the file is uploaded and processed.
226
     */
227
    handleUploadedFile = async(files) => {
228
        try {
229
            this.startImageLoading();
230
            const fileURL = await uploadFile(this.editor, 'image', files[0], files[0].name, (progress) => {
231
                this.updateLoaderIcon(this.root, this.langStrings, progress);
232
            });
233
            // Set the loader icon content to "loading" after the file upload completes.
234
            this.updateLoaderIcon(this.root, this.langStrings);
235
            this.filePickerCallback({url: fileURL});
236
        } catch (error) {
237
            // Handle the error.
238
            const urlWarningLabelEle = this.root.querySelector(Selectors.IMAGE.elements.urlWarning);
239
            urlWarningLabelEle.innerHTML = error.error !== undefined ? error.error : error;
240
            showElements(Selectors.IMAGE.elements.urlWarning, this.root);
241
            this.stopImageLoading();
242
        }
243
    };
244
 
245
    registerEventListeners() {
246
        this.root.addEventListener('click', async(e) => {
247
            const addUrlEle = e.target.closest(Selectors.IMAGE.actions.addUrl);
248
            if (addUrlEle) {
249
                this.urlChanged();
250
            }
251
 
252
            const imageBrowserAction = e.target.closest(Selectors.IMAGE.actions.imageBrowser);
253
            if (imageBrowserAction && this.canShowFilePicker) {
254
                e.preventDefault();
255
                const params = await displayFilepicker(this.editor, 'image');
256
                this.filePickerCallback(params);
257
            }
258
        });
259
 
260
        this.root.addEventListener('input', (e) => {
261
            const urlEle = e.target.closest(Selectors.IMAGE.elements.url);
262
            if (urlEle) {
263
                this.toggleUrlButton();
264
            }
265
        });
266
 
267
        const fileInput = this.root.querySelector(Selectors.IMAGE.elements.fileInput);
268
        if (fileInput) {
269
            fileInput.addEventListener('change', () => {
270
                this.handleUploadedFile(fileInput.files);
271
            });
272
        }
273
    }
274
}