MDL-68116 atto_h5p: simplify fields to add H5P content
[moodle.git] / lib / editor / atto / plugins / h5p / yui / build / moodle-atto_h5p-button / moodle-atto_h5p-button.js
blob98d1d42fbace4fb23781293605e4d303a56109e3
1 YUI.add('moodle-atto_h5p-button', function (Y, NAME) {
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
19  * @package    atto_h5p
20  * @copyright  2019 Bas Brands  <bas@moodle.com>
21  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22  */
24 /**
25  * @module moodle-atto_h5p-button
26  */
28 /**
29  * Atto h5p content tool.
30  *
31  * @namespace M.atto_h5p
32  * @class Button
33  * @extends M.editor_atto.EditorPlugin
34  */
36 var CSS = {
37         CONTENTWARNING: 'att_h5p_contentwarning',
38         H5PBROWSER: 'openh5pbrowser',
39         INPUTALT: 'atto_h5p_altentry',
40         INPUTH5PFILE: 'atto_h5p_file',
41         INPUTSUBMIT: 'atto_h5p_urlentrysubmit',
42         OPTION_DOWNLOAD_BUTTON: 'atto_h5p_option_download_button',
43         OPTION_COPYRIGHT_BUTTON: 'atto_h5p_option_copyright_button',
44         OPTION_EMBED_BUTTON: 'atto_h5p_option_embed_button',
45         URLWARNING: 'atto_h5p_warning'
46     },
47     SELECTORS = {
48         CONTENTWARNING: '.' + CSS.CONTENTWARNING,
49         H5PBROWSER: '.' + CSS.H5PBROWSER,
50         INPUTH5PFILE: '.' + CSS.INPUTH5PFILE,
51         INPUTSUBMIT: '.' + CSS.INPUTSUBMIT,
52         OPTION_DOWNLOAD_BUTTON: '.' + CSS.OPTION_DOWNLOAD_BUTTON,
53         OPTION_COPYRIGHT_BUTTON: '.' + CSS.OPTION_COPYRIGHT_BUTTON,
54         OPTION_EMBED_BUTTON: '.' + CSS.OPTION_EMBED_BUTTON,
55         URLWARNING: '.' + CSS.URLWARNING
56     },
58     COMPONENTNAME = 'atto_h5p',
60     TEMPLATE = '' +
61             '<form class="atto_form mform" id="{{elementid}}_atto_h5p_form">' +
62                 '<div style="display:none" role="alert" class="alert alert-warning mb-1 {{CSS.CONTENTWARNING}}">' +
63                     '{{get_string "noh5pcontent" component}}' +
64                 '</div>' +
65                 '<div style="display:none" role="alert" class="alert alert-warning mb-1 {{CSS.URLWARNING}}">' +
66                     '{{get_string "invalidh5purl" component}}' +
67                 '</div>' +
68                 '{{#if canUploadAndEmbed}}' +
69                     '<div class="mt-2 mb-4 attoh5pinstructions">{{{get_string "instructions" component}}}</div>' +
70                 '{{/if}}' +
71                 '<div class="mb-4">' +
72                     '<label for="{{elementid}}_{{CSS.H5PBROWSER}}">' +
73                         '{{#if canUploadAndEmbed}}' +
74                             '{{get_string "h5pfileorurl" component}}' +
75                         '{{/if}}' +
76                         '{{^if canUploadAndEmbed}}' +
77                             '{{#if canUpload}}' +
78                                 '{{get_string "h5pfile" component}}' +
79                             '{{/if}}' +
80                             '{{#if canEmbed}}' +
81                                 '{{get_string "h5purl" component}}' +
82                             '{{/if}}' +
83                         '{{/if}}' +
84                     '</label>' +
85                     '<div class="input-group input-append w-100">' +
86                         '<input class="form-control {{CSS.INPUTH5PFILE}}" type="url" value="{{fileURL}}" ' +
87                         'id="{{elementid}}_{{CSS.INPUTH5PFILE}}" data-region="h5pfile" size="32"/>' +
88                         '{{#if canUpload}}' +
89                             '<span class="input-group-append">' +
90                                 '<button class="btn btn-secondary {{CSS.H5PBROWSER}}" type="button">' +
91                                 '{{get_string "browserepositories" component}}</button>' +
92                             '</span>' +
93                         '{{/if}}' +
94                     '</div>' +
95                     '{{#if canUpload}}' +
96                         '<fieldset class="collapsible {{#if collapseOptions}}collapsed{{/if}}" id="{{elementid}}_h5poptions">' +
97                             '<legend class="ftoggler">{{get_string "h5poptions" component}}</legend>' +
98                             '<div class="fcontainer">' +
99                                 '<div class="form-check">' +
100                                     '<input type="checkbox" {{optionDownloadButton}} ' +
101                                     'class="form-check-input {{CSS.OPTION_DOWNLOAD_BUTTON}}"' +
102                                     'aria-label="{{get_string "downloadbutton" component}}" ' +
103                                     'id="{{elementid}}_h5p-option-allow-download"/>' +
104                                     '<label class="form-check-label" for="{{elementid}}_h5p-option-allow-download">' +
105                                     '{{get_string "downloadbutton" component}}' +
106                                     '</label>' +
107                                 '</div>' +
108                                 '<div class="form-check">' +
109                                     '<input type="checkbox" {{optionEmbedButton}} ' +
110                                     'class="form-check-input {{CSS.OPTION_EMBED_BUTTON}}" ' +
111                                     'aria-label="{{get_string "embedbutton" component}}" ' +
112                                         'id="{{elementid}}_h5p-option-embed-button"/>' +
113                                     '<label class="form-check-label" for="{{elementid}}_h5p-option-embed-button">' +
114                                     '{{get_string "embedbutton" component}}' +
115                                     '</label>' +
116                                 '</div>' +
117                                 '<div class="form-check mb-2">' +
118                                     '<input type="checkbox" {{optionCopyrightButton}} ' +
119                                     'class="form-check-input {{CSS.OPTION_COPYRIGHT_BUTTON}}" ' +
120                                     'aria-label="{{get_string "copyrightbutton" component}}" ' +
121                                         'id="{{elementid}}_h5p-option-copyright-button"/>' +
122                                     '<label class="form-check-label" for="{{elementid}}_h5p-option-copyright-button">' +
123                                     '{{get_string "copyrightbutton" component}}' +
124                                     '</label>' +
125                                 '</div>' +
126                             '</div>' +
127                         '</fieldset>' +
128                     '{{/if}}' +
129                 '</div>' +
130                 '<div class="text-center">' +
131                 '<button class="btn btn-secondary {{CSS.INPUTSUBMIT}}" type="submit">' + '' +
132                     '{{get_string "pluginname" component}}</button>' +
133                 '</div>' +
134             '</form>',
136         H5PTEMPLATE = '' +
137             '{{#if addParagraphs}}<p><br></p>{{/if}}' +
138             '<div class="h5p-placeholder" contenteditable="false">' +
139                 '{{{url}}}' +
140             '</div>' +
141             '{{#if addParagraphs}}<p><br></p>{{/if}}';
143 Y.namespace('M.atto_h5p').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
144     /**
145      * A reference to the current selection at the time that the dialogue
146      * was opened.
147      *
148      * @property _currentSelection
149      * @type Range
150      * @private
151      */
152     _currentSelection: null,
154     /**
155      * A reference to the currently open form.
156      *
157      * @param _form
158      * @type Node
159      * @private
160      */
161     _form: null,
163     /**
164      * A reference to the currently selected H5P div.
165      *
166      * @param _form
167      * @type Node
168      * @private
169      */
170     _H5PDiv: null,
172     /**
173      * Allowed methods of adding H5P.
174      *
175      * @param _allowedmethods
176      * @type String
177      * @private
178      */
179     _allowedmethods: 'none',
181     initializer: function() {
182         this._allowedmethods = this.get('allowedmethods');
183         if (this._allowedmethods === 'none') {
184             // Plugin not available here.
185             return;
186         }
187         this.addButton({
188             icon: 'icon',
189             iconComponent: 'atto_h5p',
190             callback: this._displayDialogue,
191             tags: '.h5p-placeholder',
192             tagMatchRequiresAll: false
193         });
195         this.editor.all('.h5p-placeholder').setAttribute('contenteditable', 'false');
196         this.editor.delegate('dblclick', this._handleDblClick, '.h5p-placeholder', this);
197         this.editor.delegate('click', this._handleClick, '.h5p-placeholder', this);
198     },
200     /**
201      * Handle a double click on a H5P Placeholder.
202      *
203      * @method _handleDblClick
204      * @private
205      */
206     _handleDblClick: function() {
207         this._displayDialogue();
208     },
210     /**
211      * Handle a click on a H5P Placeholder.
212      *
213      * @method _handleClick
214      * @param {EventFacade} e
215      * @private
216      */
217     _handleClick: function(e) {
218         var selection = this.get('host').getSelectionFromNode(e.target);
219         if (this.get('host').getSelection() !== selection) {
220             this.get('host').setSelection(selection);
221         }
222     },
224     /**
225      * Display the h5p editing tool.
226      *
227      * @method _displayDialogue
228      * @private
229      */
230     _displayDialogue: function() {
231         // Store the current selection.
232         this._currentSelection = this.get('host').getSelection();
234         if (this._currentSelection === false) {
235             return;
236         }
238         this._getH5PDiv();
240         var dialogue = this.getDialogue({
241             headerContent: M.util.get_string('pluginname', COMPONENTNAME),
242             width: 'auto',
243             focusAfterHide: true
244         });
245         // Set the dialogue content, and then show the dialogue.
246         dialogue.set('bodyContent', this._getDialogueContent())
247             .show();
248         M.form.shortforms({formid: this.get('host').get('elementid') + '_atto_h5p_form'});
249     },
251     /**
252      * Get the H5P iframe
253      *
254      * @method _resolveH5P
255      * @return {Node} The H5P iframe selected.
256      * @private
257      */
258     _getH5PDiv: function() {
259         var selectednodes = this.get('host').getSelectedNodes();
260         var H5PDiv = null;
261         selectednodes.each(function(selNode) {
262             if (selNode.hasClass('h5p-placeholder')) {
263                 H5PDiv = selNode;
264             }
265         });
266         this._H5PDiv = H5PDiv;
267     },
269     /**
270      * Get the H5P button permissions.
271      *
272      * @return {Object} H5P button permissions.
273      * @private
274      */
275     _getPermissions: function() {
276         var permissions = {
277             'canEmbed': false,
278             'canUpload': false,
279             'canUploadAndEmbed': false
280         };
282         if (this.get('host').canShowFilepicker('h5p')) {
283             if (this._allowedmethods === 'both') {
284                 permissions.canUploadAndEmbed = true;
285                 permissions.canUpload = true;
286             } else if (this._allowedmethods === 'upload') {
287                 permissions.canUpload = true;
288             }
289         }
291         if (this._allowedmethods === 'both' || this._allowedmethods === 'embed') {
292             permissions.canEmbed = true;
293         }
294         return permissions;
295     },
298     /**
299      * Return the dialogue content for the tool, attaching any required
300      * events.
301      *
302      * @method _getDialogueContent
303      * @return {Node} The content to place in the dialogue.
304      * @private
305      */
306     _getDialogueContent: function() {
308         var permissions = this._getPermissions();
310         var fileURL,
311             optionDownloadButton,
312             optionEmbedButton,
313             optionCopyrightButton,
314             collapseOptions = true;
316         if (this._H5PDiv) {
317             var H5PURL = this._H5PDiv.get('innerHTML');
318             var fileBaseUrl = M.cfg.wwwroot + '/draftfile.php';
319             if (fileBaseUrl == H5PURL.substring(0, fileBaseUrl.length)) {
320                 fileURL = H5PURL.split("?")[0];
322                 var parameters = H5PURL.split("?")[1];
323                 if (parameters) {
324                     if (parameters.match(/export=1/)) {
325                         optionDownloadButton = 'checked';
326                         collapseOptions = false;
327                     }
329                     if (parameters.match(/embed=1/)) {
330                         optionEmbedButton = 'checked';
331                         collapseOptions = false;
332                     }
334                     if (parameters.match(/copyright=1/)) {
335                         optionCopyrightButton = 'checked';
336                         collapseOptions = false;
337                     }
338                 }
339             } else {
340                 fileURL = H5PURL;
341             }
342         }
344         var template = Y.Handlebars.compile(TEMPLATE),
345             content = Y.Node.create(template({
346                 elementid: this.get('host').get('elementid'),
347                 CSS: CSS,
348                 component: COMPONENTNAME,
349                 canUpload: permissions.canUpload,
350                 canEmbed: permissions.canEmbed,
351                 canUploadAndEmbed: permissions.canUploadAndEmbed,
352                 collapseOptions: collapseOptions,
353                 fileURL: fileURL,
354                 optionDownloadButton: optionDownloadButton,
355                 optionEmbedButton: optionEmbedButton,
356                 optionCopyrightButton: optionCopyrightButton
357             }));
359         this._form = content;
361         // Listen to and act on Dialogue content events.
362         this._setEventListeners();
364         return content;
365     },
367     /**
368      * Update the dialogue after an h5p was selected in the File Picker.
369      *
370      * @method _filepickerCallback
371      * @param {object} params The parameters provided by the filepicker
372      * containing information about the h5p.
373      * @private
374      */
375     _filepickerCallback: function(params) {
376         if (params.url !== '') {
377             var input = this._form.one(SELECTORS.INPUTH5PFILE);
378             input.set('value', params.url);
379             this._removeWarnings();
380         }
381     },
383     /**
384      * Set event Listeners for Dialogue content actions.
385      *
386      * @method  _setEventListeners
387      * @private
388      */
389     _setEventListeners: function() {
390         var form = this._form;
391         var permissions = this._getPermissions();
393         form.one(SELECTORS.INPUTSUBMIT).on('click', this._setH5P, this);
395         if (permissions.canUpload) {
396             form.one(SELECTORS.H5PBROWSER).on('click', function() {
397                 this.get('host').showFilepicker('h5p', this._filepickerCallback, this);
398             }, this);
399         }
401         if (permissions.canUploadAndEmbed) {
402             form.one(SELECTORS.INPUTH5PFILE).on('change', function() {
403                 this._removeWarnings();
404             }, this);
405         }
406     },
408     /**
409      * Remove warnings shown in the dialogue.
410      *
411      * @method _removeWarnings
412      * @private
413      */
414     _removeWarnings: function() {
415         var form = this._form;
416         form.one(SELECTORS.URLWARNING).setStyle('display', 'none');
417         form.one(SELECTORS.CONTENTWARNING).setStyle('display', 'none');
418     },
420     /**
421      * Update the h5p in the contenteditable.
422      *
423      * @method _setH5P
424      * @param {EventFacade} e
425      * @private
426      */
427     _setH5P: function(e) {
428         var form = this._form,
429             h5phtml,
430             host = this.get('host'),
431             h5pfile = form.one(SELECTORS.INPUTH5PFILE).get('value'),
432             permissions = this._getPermissions();
434         e.preventDefault();
436         // Check if there are any issues.
437         if (this._updateWarning()) {
438             return;
439         }
441         // Focus on the editor in preparation for inserting the H5P.
442         host.focus();
444         // Add an empty paragraph after new H5P container that can catch the cursor.
445         var addParagraphs = true;
447         // If a H5P placeholder was selected we can destroy it now.
448         if (this._H5PDiv) {
449             this._H5PDiv.remove();
450             addParagraphs = false;
451         }
453         if (h5pfile !== '') {
454             host.setSelection(this._currentSelection);
456             if (h5pfile.startsWith(M.cfg.wwwroot)) {
457                 // It's a local file.
458                 var params = '';
459                 if (permissions.canUpload) {
460                     var options = {};
461                     if (form.one(SELECTORS.OPTION_DOWNLOAD_BUTTON).get('checked')) {
462                         options['export'] = '1';
463                     }
464                     if (form.one(SELECTORS.OPTION_EMBED_BUTTON).get('checked')) {
465                         options.embed = '1';
466                     }
467                     if (form.one(SELECTORS.OPTION_COPYRIGHT_BUTTON).get('checked')) {
468                         options.copyright = '1';
469                     }
471                     for (var opt in options) {
472                         if (params === "" && (h5pfile.indexOf("?") === -1)) {
473                             params += "?";
474                         } else {
475                             params += "&amp;";
476                         }
477                         params += opt + "=" + options[opt];
478                     }
479                 }
481                 var h5ptemplate = Y.Handlebars.compile(H5PTEMPLATE);
483                 h5phtml = h5ptemplate({
484                     url: h5pfile + params,
485                     addParagraphs: addParagraphs
486                 });
487             } else {
488                 // It's a URL.
489                 var urltemplate = Y.Handlebars.compile(H5PTEMPLATE);
490                 h5phtml = urltemplate({
491                     url: h5pfile
492                 });
493             }
495             host.insertContentAtFocusPoint(h5phtml);
497             this.markUpdated();
498         }
500         this.getDialogue({
501             focusAfterHide: null
502         }).hide();
503     },
505     /**
506      * Check if this could be a h5p embed.
507      *
508      * @method _validEmbed
509      * @param {String} str
510      * @return {boolean} whether this is a iframe tag.
511      * @private
512      */
513     _validEmbed: function(str) {
514         var pattern = new RegExp('^(<iframe).*(<\\/iframe>)'); // Port and path.
515         return !!pattern.test(str);
516     },
518     /**
519      * Check if this could be a h5p URL.
520      *
521      * @method _validURL
522      * @param {String} str
523      * @return {boolean} whether this is a valid URL.
524      * @private
525      */
526     _validURL: function(str) {
527         var pattern = new RegExp('^(https?:\\/\\/)?' + // Protocol.
528             '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // Domain name.
529             '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address.
530             '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'); // Port and path.
531         return !!pattern.test(str);
532     },
534     /**
535      * Update the url warning.
536      *
537      * @method _updateWarning
538      * @return {boolean} whether a warning should be displayed.
539      * @private
540      */
541     _updateWarning: function() {
542         var form = this._form,
543             state = true,
544             h5pfile,
545             permissions = this._getPermissions();
547         if (permissions.canUpload || permissions.canEmbed) {
548             h5pfile = form.one(SELECTORS.INPUTH5PFILE).get('value');
549             if (h5pfile !== '') {
550                 form.one(SELECTORS.CONTENTWARNING).setStyle('display', 'none');
551                 if (h5pfile.startsWith(M.cfg.wwwroot) || this._validURL(h5pfile)) {
552                     // Only external URLs have to be validated.
553                     form.one(SELECTORS.URLWARNING).setStyle('display', 'none');
554                     state = false;
555                 } else {
556                     form.one(SELECTORS.URLWARNING).setStyle('display', 'block');
557                     state = true;
558                 }
559             } else {
560                 form.one(SELECTORS.CONTENTWARNING).setStyle('display', 'block');
561                 state = true;
562             }
563         }
565         return state;
566     }
567 }, {
568     ATTRS: {
569         /**
570          * The allowedmethods of adding h5p content.
571          *
572          * @attribute allowedmethods
573          * @type String
574          */
575         allowedmethods: {
576             value: null
577         }
578     }
582 }, '@VERSION@', {"requires": ["moodle-editor_atto-plugin"]});