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 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 ?? '',
65
    }, data);
66
};
67
 
68
/**
69
 * Get the URL from the submitted form.
70
 *
71
 * @param {FormNode} form
72
 * @param {string} submittedUrl
73
 * @returns {URL|null}
74
 */
75
const getUrlFromSubmission = (form, submittedUrl) => {
76
    if (!submittedUrl || (!submittedUrl.startsWith(Config.wwwroot) && !isValidUrl(submittedUrl))) {
77
        return null;
78
    }
79
 
80
    // Generate a URL Object for the submitted URL.
81
    const url = new URL(submittedUrl);
82
 
83
    const downloadElement = form.querySelector('[name="download"]');
84
    if (downloadElement?.checked) {
85
        url.searchParams.append('export', 1);
86
    }
87
 
88
    const embedElement = form.querySelector('[name="embed"]');
89
    if (embedElement?.checked) {
90
        url.searchParams.append('embed', 1);
91
    }
92
 
93
    const copyrightElement = form.querySelector('[name="copyright"]');
94
    if (copyrightElement?.checked) {
95
        url.searchParams.append('copyright', 1);
96
    }
97
 
98
    return url;
99
};
100
 
101
/**
102
 * Verify if this could be a h5p URL.
103
 *
104
 * @param {string} url Url to verify
105
 * @return {boolean} whether this is a valid URL.
106
 */
107
const isValidUrl = (url) => {
108
    const pattern = new RegExp('^(https?:\\/\\/)?' + // Protocol.
109
        '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // Domain name.
110
        '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address.
111
        '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'); // Port and path.
112
    return !!pattern.test(url);
113
};
114
 
115
const handleDialogueSubmission = async(editor, modal, data) => {
116
    const pendingPromise = new Pending('tiny_h5p:handleDialogueSubmission');
117
 
118
    const form = getList(modal.getRoot())[0].querySelector('form');
119
    if (!form) {
120
        // The form couldn't be found, which is weird.
121
        // This should not happen.
122
        // Display the dialogue again.
123
        modal.destroy();
124
        displayDialogue(editor, Object.assign({}, data));
125
        pendingPromise.resolve();
126
        return;
127
    }
128
 
129
    // Get the URL from the submitted form.
130
    const submittedUrl = form.querySelector('input[name="url"]').value;
131
    const url = getUrlFromSubmission(form, submittedUrl);
132
 
133
    if (!url) {
134
        // The URL is invalid.
135
        // Fill it in and represent the dialogue with an error.
136
        modal.destroy();
137
        displayDialogue(editor, Object.assign({}, data, {
138
            url: submittedUrl,
139
            invalidUrl: true,
140
        }));
141
        pendingPromise.resolve();
142
        return;
143
    }
144
 
145
    const content = await renderForPromise(`${component}/content`, {
146
        url: url.toString(),
147
    });
148
 
149
    editor.selection.moveToBookmark(openingSelection);
150
    editor.execCommand('mceInsertContent', false, content.html);
151
    editor.selection.moveToBookmark(openingSelection);
152
    pendingPromise.resolve();
153
};
154
 
155
const getCurrentH5PData = (currentH5P) => {
156
    const data = {};
157
    let url;
158
    try {
159
        url = new URL(currentH5P.textContent);
160
    } catch (error) {
161
        return data;
162
    }
163
 
164
    if (url.searchParams.has('export')) {
165
        data.download = true;
166
        data.showOptions = true;
167
        url.searchParams.delete('export');
168
    }
169
 
170
    if (url.searchParams.has('embed')) {
171
        data.embed = true;
172
        data.showOptions = true;
173
        url.searchParams.delete('embed');
174
    }
175
 
176
    if (url.searchParams.has('copyright')) {
177
        data.copyright = true;
178
        data.showOptions = true;
179
        url.searchParams.delete('copyright');
180
    }
181
 
182
    data.url = url.toString();
183
 
184
    return data;
185
};
186
 
187
const displayDialogue = async(editor, data = {}) => {
188
    const selection = editor.selection.getNode();
189
    const currentH5P = selection.closest('.h5p-placeholder');
190
    if (currentH5P) {
191
        Object.assign(data, getCurrentH5PData(currentH5P));
192
    }
193
 
194
    const modal = await Modal.create({
195
        templateContext: getTemplateContext(editor, data),
196
    });
197
 
198
    const $root = modal.getRoot();
199
    const root = $root[0];
200
    $root.on(ModalEvents.save, (event, modal) => {
201
        handleDialogueSubmission(editor, modal, data);
202
    });
203
 
204
    root.addEventListener('click', (e) => {
205
        const filepickerButton = e.target.closest('[data-target="filepicker"]');
206
        if (filepickerButton) {
207
            displayFilepicker(editor, 'h5p').then((params) => {
208
                if (params.url !== '') {
209
                    const input = root.querySelector('form input[name="url"]');
210
                    input.value = params.url;
211
                }
212
                return params;
213
            })
214
                .catch();
215
        }
216
    });
217
};