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 H5P Content configuration.
18
 *
19
 * @module      tiny_h5p/ui
20
 * @copyright   2022 Andrew Lyons <andrew@nicols.co.uk>
21
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22
 */
23
 
24
import {displayFilepicker} from 'editor_tiny/utils';
25
import {component} from './common';
26
import {getPermissions} from './options';
27
 
28
import Config from 'core/config';
29
import {getList} from 'core/normalise';
30
import {renderForPromise} from 'core/templates';
31
import Modal from 'tiny_h5p/modal';
32
import ModalEvents from 'core/modal_events';
33
import Pending from 'core/pending';
34
import {getFilePicker} from 'editor_tiny/options';
35
 
36
let openingSelection = null;
37
 
38
export const handleAction = (editor) => {
39
    openingSelection = editor.selection.getBookmark();
40
    displayDialogue(editor);
41
};
42
 
43
/**
44
 * Get the template context for the dialogue.
45
 *
46
 * @param {Editor} editor
47
 * @param {object} data
48
 * @returns {object} data
49
 */
50
const getTemplateContext = (editor, data) => {
51
    const permissions = getPermissions(editor);
52
 
53
    const canShowFilePicker = typeof getFilePicker(editor, 'h5p') !== 'undefined';
54
    const canUpload = (permissions.upload && canShowFilePicker) ?? false;
55
    const canEmbed = permissions.embed ?? false;
56
    const canUploadAndEmbed = canUpload && canEmbed;
57
 
58
    return Object.assign({}, {
59
        elementid: editor.id,
60
        canUpload,
61
        canEmbed,
62
        canUploadAndEmbed,
63
        showOptions: false,
64
        fileURL: data?.url ?? '',
1441 ariadna 65
        showDisplayOptions: false,
1 efrain 66
    }, data);
67
};
68
 
69
/**
70
 * Get the URL from the submitted form.
71
 *
72
 * @param {FormNode} form
73
 * @param {string} submittedUrl
74
 * @returns {URL|null}
75
 */
76
const getUrlFromSubmission = (form, submittedUrl) => {
77
    if (!submittedUrl || (!submittedUrl.startsWith(Config.wwwroot) && !isValidUrl(submittedUrl))) {
78
        return null;
79
    }
80
 
81
    // Generate a URL Object for the submitted URL.
82
    const url = new URL(submittedUrl);
83
 
84
    const downloadElement = form.querySelector('[name="download"]');
85
    if (downloadElement?.checked) {
86
        url.searchParams.append('export', 1);
87
    }
88
 
89
    const embedElement = form.querySelector('[name="embed"]');
90
    if (embedElement?.checked) {
91
        url.searchParams.append('embed', 1);
92
    }
93
 
94
    const copyrightElement = form.querySelector('[name="copyright"]');
95
    if (copyrightElement?.checked) {
96
        url.searchParams.append('copyright', 1);
97
    }
98
 
99
    return url;
100
};
101
 
102
/**
103
 * Verify if this could be a h5p URL.
104
 *
105
 * @param {string} url Url to verify
106
 * @return {boolean} whether this is a valid URL.
107
 */
108
const isValidUrl = (url) => {
109
    const pattern = new RegExp('^(https?:\\/\\/)?' + // Protocol.
110
        '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // Domain name.
111
        '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address.
112
        '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'); // Port and path.
113
    return !!pattern.test(url);
114
};
115
 
116
const handleDialogueSubmission = async(editor, modal, data) => {
117
    const pendingPromise = new Pending('tiny_h5p:handleDialogueSubmission');
118
 
119
    const form = getList(modal.getRoot())[0].querySelector('form');
120
    if (!form) {
121
        // The form couldn't be found, which is weird.
122
        // This should not happen.
123
        // Display the dialogue again.
124
        modal.destroy();
125
        displayDialogue(editor, Object.assign({}, data));
126
        pendingPromise.resolve();
127
        return;
128
    }
129
 
130
    // Get the URL from the submitted form.
131
    const submittedUrl = form.querySelector('input[name="url"]').value;
132
    const url = getUrlFromSubmission(form, submittedUrl);
133
 
134
    if (!url) {
135
        // The URL is invalid.
136
        // Fill it in and represent the dialogue with an error.
137
        modal.destroy();
138
        displayDialogue(editor, Object.assign({}, data, {
139
            url: submittedUrl,
140
            invalidUrl: true,
141
        }));
142
        pendingPromise.resolve();
143
        return;
144
    }
145
 
1441 ariadna 146
    const mobileAppAutoPlay = form.querySelector('[name="mobileappautoplay"]')?.checked;
147
 
1 efrain 148
    const content = await renderForPromise(`${component}/content`, {
149
        url: url.toString(),
1441 ariadna 150
        mobileAppAutoPlay,
1 efrain 151
    });
152
 
153
    editor.selection.moveToBookmark(openingSelection);
154
    editor.execCommand('mceInsertContent', false, content.html);
155
    editor.selection.moveToBookmark(openingSelection);
156
    pendingPromise.resolve();
157
};
158
 
159
const getCurrentH5PData = (currentH5P) => {
160
    const data = {};
161
    let url;
162
    try {
163
        url = new URL(currentH5P.textContent);
164
    } catch (error) {
165
        return data;
166
    }
167
 
168
    if (url.searchParams.has('export')) {
169
        data.download = true;
170
        data.showOptions = true;
171
        url.searchParams.delete('export');
172
    }
173
 
174
    if (url.searchParams.has('embed')) {
175
        data.embed = true;
176
        data.showOptions = true;
177
        url.searchParams.delete('embed');
178
    }
179
 
180
    if (url.searchParams.has('copyright')) {
181
        data.copyright = true;
182
        data.showOptions = true;
183
        url.searchParams.delete('copyright');
184
    }
185
 
1441 ariadna 186
    if (currentH5P.dataset.mobileappAutoplay == 'true') {
187
        data.mobileAppAutoPlay = true;
188
        data.showDisplayOptions = true;
189
    }
1 efrain 190
 
1441 ariadna 191
     data.url = url.toString();
192
 
1 efrain 193
    return data;
194
};
195
 
196
const displayDialogue = async(editor, data = {}) => {
197
    const selection = editor.selection.getNode();
198
    const currentH5P = selection.closest('.h5p-placeholder');
199
    if (currentH5P) {
200
        Object.assign(data, getCurrentH5PData(currentH5P));
201
    }
202
 
203
    const modal = await Modal.create({
204
        templateContext: getTemplateContext(editor, data),
205
    });
206
 
207
    const $root = modal.getRoot();
208
    const root = $root[0];
209
    $root.on(ModalEvents.save, (event, modal) => {
210
        handleDialogueSubmission(editor, modal, data);
211
    });
212
 
213
    root.addEventListener('click', (e) => {
214
        const filepickerButton = e.target.closest('[data-target="filepicker"]');
215
        if (filepickerButton) {
216
            displayFilepicker(editor, 'h5p').then((params) => {
217
                if (params.url !== '') {
218
                    const input = root.querySelector('form input[name="url"]');
219
                    input.value = params.url;
220
                }
221
                return params;
222
            })
223
                .catch();
224
        }
225
    });
226
};