MDL-67675 atto_h5p: No need to manually clear the H5P placeholder
[moodle.git] / lib / editor / atto / plugins / h5p / yui / src / button / js / button.js
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/>.
16 /*
17  * @package    atto_h5p
18  * @copyright  2019 Bas Brands  <bas@moodle.com>
19  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
20  */
22 /**
23  * @module moodle-atto_h5p-button
24  */
26 /**
27  * Atto h5p content tool.
28  *
29  * @namespace M.atto_h5p
30  * @class Button
31  * @extends M.editor_atto.EditorPlugin
32  */
34 var CSS = {
35         CONTENTWARNING: 'att_h5p_contentwarning',
36         H5PBROWSER: 'openh5pbrowser',
37         INPUTALT: 'atto_h5p_altentry',
38         INPUTH5PFILE: 'atto_h5p_file',
39         INPUTH5PURL: 'atto_h5p_url',
40         INPUTSUBMIT: 'atto_h5p_urlentrysubmit',
41         OPTION_DOWNLOAD_BUTTON: 'atto_h5p_option_download_button',
42         OPTION_COPYRIGHT_BUTTON: 'atto_h5p_option_copyright_button',
43         OPTION_EMBED_BUTTON: 'atto_h5p_option_embed_button',
44         URLWARNING: 'atto_h5p_warning'
45     },
46     SELECTORS = {
47         CONTENTWARNING: '.' + CSS.CONTENTWARNING,
48         H5PBROWSER: '.' + CSS.H5PBROWSER,
49         INPUTH5PFILE: '.' + CSS.INPUTH5PFILE,
50         INPUTH5PURL: '.' + CSS.INPUTH5PURL,
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                 '{{#if canUploadAndEmbed}}' +
66                     '<div class="mt-2 attoh5pinstructions">{{{get_string "instructions" component}}}</div>' +
67                     '<div class="my-2"><strong>{{get_string "either" component}}</strong></div>' +
68                 '{{/if}}' +
69                 '{{#if canEmbed}}' +
70                 '<div class="mb-4">' +
71                     '<label for="{{elementid}}_{{CSS.INPUTH5PURL}}">{{get_string "enterurl" component}}</label>' +
72                     '<div style="display:none" role="alert" class="alert alert-warning mb-1 {{CSS.URLWARNING}}">' +
73                         '{{get_string "invalidh5purl" component}}' +
74                     '</div>' +
75                     '<textarea rows="3" data-region="h5purl" class="form-control {{CSS.INPUTH5PURL}}" type="url" ' +
76                     'id="{{elementid}}_{{CSS.INPUTH5PURL}}" />{{embedURL}}</textarea>' +
77                 '</div>' +
78                 '{{/if}}' +
79                 '{{#if canUploadAndEmbed}}' +
80                     '<div class="my-2"><strong>{{get_string "or" component}}</strong></div>' +
81                 '{{/if}}' +
82                 '{{#if canUpload}}' +
83                 '<div class="mb-4">' +
84                     '<label for="{{elementid}}_{{CSS.H5PBROWSER}}">{{get_string "h5pfile" component}}</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}}" size="32"/>' +
88                         '<span class="input-group-append">' +
89                             '<button class="btn btn-secondary {{CSS.H5PBROWSER}}" type="button">' +
90                             '{{get_string "browserepositories" component}}</button>' +
91                         '</span>' +
92                     '</div>' +
93                     '<fieldset class="collapsible {{#if collapseOptions}}collapsed{{/if}}" id="{{elementid}}_h5poptions">' +
94                         '<legend class="ftoggler">{{get_string "h5poptions" component}}</legend>' +
95                         '<div class="fcontainer">' +
96                             '<div class="form-check">' +
97                                 '<input type="checkbox" {{optionDownloadButton}} ' +
98                                 'class="form-check-input {{CSS.OPTION_DOWNLOAD_BUTTON}}"' +
99                                 'id="{{elementid}}_h5p-option-allow-download"/>' +
100                                 '<label class="form-check-label" for="{{elementid}}_h5p-option-allow-download">' +
101                                 '{{get_string "downloadbutton" component}}' +
102                                 '</label>' +
103                             '</div>' +
104                             '<div class="form-check">' +
105                                 '<input type="checkbox" {{optionEmbedButton}} ' +
106                                 'class="form-check-input {{CSS.OPTION_EMBED_BUTTON}}" ' +
107                                     'id="{{elementid}}_h5p-option-embed-button"/>' +
108                                 '<label class="form-check-label" for="{{elementid}}_h5p-option-embed-button">' +
109                                 '{{get_string "embedbutton" component}}' +
110                                 '</label>' +
111                             '</div>' +
112                             '<div class="form-check mb-2">' +
113                                 '<input type="checkbox" {{optionCopyrightButton}} ' +
114                                 'class="form-check-input {{CSS.OPTION_COPYRIGHT_BUTTON}}" ' +
115                                     'id="{{elementid}}_h5p-option-copyright-button"/>' +
116                                 '<label class="form-check-label" for="{{elementid}}_h5p-option-copyright-button">' +
117                                 '{{get_string "copyrightbutton" component}}' +
118                                 '</label>' +
119                             '</div>' +
120                         '</div>' +
121                     '</fieldset>' +
122                 '</div>' +
123                 '{{/if}}' +
124                 '<div class="text-center">' +
125                 '<button class="btn btn-secondary {{CSS.INPUTSUBMIT}}" type="submit">' + '' +
126                     '{{get_string "pluginname" component}}</button>' +
127                 '</div>' +
128             '</form>',
130         H5PTEMPLATE = '' +
131             '{{#if addParagraphs}}<p><br></p>{{/if}}' +
132             '<div class="h5p-placeholder" contenteditable="false">' +
133                 '{{{url}}}' +
134             '</div>' +
135             '{{#if addParagraphs}}<p><br></p>{{/if}}';
137 Y.namespace('M.atto_h5p').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
138     /**
139      * A reference to the current selection at the time that the dialogue
140      * was opened.
141      *
142      * @property _currentSelection
143      * @type Range
144      * @private
145      */
146     _currentSelection: null,
148     /**
149      * A reference to the currently open form.
150      *
151      * @param _form
152      * @type Node
153      * @private
154      */
155     _form: null,
157     /**
158      * A reference to the currently selected H5P div.
159      *
160      * @param _form
161      * @type Node
162      * @private
163      */
164     _H5PDiv: null,
166     /**
167      * Allowed methods of adding H5P.
168      *
169      * @param _allowedmethods
170      * @type String
171      * @private
172      */
173     _allowedmethods: 'none',
175     initializer: function() {
176         this._allowedmethods = this.get('allowedmethods');
177         if (this._allowedmethods === 'none') {
178             // Plugin not available here.
179             return;
180         }
181         this.addButton({
182             icon: 'icon',
183             iconComponent: 'atto_h5p',
184             callback: this._displayDialogue,
185             tags: '.h5p-placeholder',
186             tagMatchRequiresAll: false
187         });
189         this.editor.all('.h5p-placeholder').setAttribute('contenteditable', 'false');
190         this.editor.delegate('dblclick', this._handleDblClick, '.h5p-placeholder', this);
191         this.editor.delegate('click', this._handleClick, '.h5p-placeholder', this);
192     },
194     /**
195      * Handle a double click on a H5P Placeholder.
196      *
197      * @method _handleDblClick
198      * @private
199      */
200     _handleDblClick: function() {
201         this._displayDialogue();
202     },
204     /**
205      * Handle a click on a H5P Placeholder.
206      *
207      * @method _handleClick
208      * @param {EventFacade} e
209      * @private
210      */
211     _handleClick: function(e) {
212         var selection = this.get('host').getSelectionFromNode(e.target);
213         if (this.get('host').getSelection() !== selection) {
214             this.get('host').setSelection(selection);
215         }
216     },
218     /**
219      * Display the h5p editing tool.
220      *
221      * @method _displayDialogue
222      * @private
223      */
224     _displayDialogue: function() {
225         // Store the current selection.
226         this._currentSelection = this.get('host').getSelection();
228         if (this._currentSelection === false) {
229             return;
230         }
232         this._getH5PDiv();
234         var dialogue = this.getDialogue({
235             headerContent: M.util.get_string('pluginname', COMPONENTNAME),
236             width: 'auto',
237             focusAfterHide: true
238         });
239         // Set the dialogue content, and then show the dialogue.
240         dialogue.set('bodyContent', this._getDialogueContent())
241             .show();
242         M.form.shortforms({formid: this.get('host').get('elementid') + '_atto_h5p_form'});
243     },
245     /**
246      * Get the H5P iframe
247      *
248      * @method _resolveH5P
249      * @return {Node} The H5P iframe selected.
250      * @private
251      */
252     _getH5PDiv: function() {
253         var selectednodes = this.get('host').getSelectedNodes();
254         var H5PDiv = null;
255         selectednodes.each(function(selNode) {
256             if (selNode.hasClass('h5p-placeholder')) {
257                 H5PDiv = selNode;
258             }
259         });
260         this._H5PDiv = H5PDiv;
261     },
263     /**
264      * Get the H5P button permissions.
265      *
266      * @return {Object} H5P button permissions.
267      * @private
268      */
269     _getPermissions: function() {
270         var permissions = {
271             'canUpload': false,
272             'canUploadAndEbmed': false,
273             'canEmbed': false
274         };
276         if (this.get('host').canShowFilepicker('h5p')) {
277             if (this._allowedmethods === 'both') {
278                 permissions.canUploadAndEmbed = true;
279                 permissions.canUpload = true;
280             } else if (this._allowedmethods === 'upload') {
281                 permissions.canUpload = true;
282             }
283         }
285         if (this._allowedmethods === 'both' || this._allowedmethods === 'embed') {
286             permissions.canEmbed = true;
287         }
288         return permissions;
289     },
292     /**
293      * Return the dialogue content for the tool, attaching any required
294      * events.
295      *
296      * @method _getDialogueContent
297      * @return {Node} The content to place in the dialogue.
298      * @private
299      */
300     _getDialogueContent: function() {
302         var permissions = this._getPermissions();
304         var fileURL,
305             embedURL,
306             optionDownloadButton,
307             optionEmbedButton,
308             optionCopyrightButton,
309             collapseOptions = true;
311         if (this._H5PDiv) {
312             var H5PURL = this._H5PDiv.get('innerHTML');
313             var fileBaseUrl = M.cfg.wwwroot + '/draftfile.php';
314             if (fileBaseUrl == H5PURL.substring(0, fileBaseUrl.length)) {
315                 fileURL = H5PURL.split("?")[0];
317                 var parameters = H5PURL.split("?")[1];
318                 if (parameters) {
319                     if (parameters.match(/export=1/)) {
320                         optionDownloadButton = 'checked';
321                         collapseOptions = false;
322                     }
324                     if (parameters.match(/embed=1/)) {
325                         optionEmbedButton = 'checked';
326                         collapseOptions = false;
327                     }
329                     if (parameters.match(/copyright=1/)) {
330                         optionCopyrightButton = 'checked';
331                         collapseOptions = false;
332                     }
333                 }
334             } else {
335                 embedURL = H5PURL;
336             }
337         }
339         var template = Y.Handlebars.compile(TEMPLATE),
340             content = Y.Node.create(template({
341                 elementid: this.get('host').get('elementid'),
342                 CSS: CSS,
343                 component: COMPONENTNAME,
344                 canUpload: permissions.canUpload,
345                 canEmbed: permissions.canEmbed,
346                 fileURL: fileURL,
347                 embedURL: embedURL,
348                 canUploadAndEmbed: permissions.canUploadAndEmbed,
349                 collapseOptions: collapseOptions,
350                 optionDownloadButton: optionDownloadButton,
351                 optionEmbedButton: optionEmbedButton,
352                 optionCopyrightButton: optionCopyrightButton
353             }));
355         this._form = content;
357         // Listen to and act on Dialogue content events.
358         this._setEventListeners();
360         return content;
361     },
363     /**
364      * Update the dialogue after an h5p was selected in the File Picker.
365      *
366      * @method _filepickerCallback
367      * @param {object} params The parameters provided by the filepicker
368      * containing information about the h5p.
369      * @private
370      */
371     _filepickerCallback: function(params) {
372         if (params.url !== '') {
373             var input = this._form.one(SELECTORS.INPUTH5PFILE);
374             input.set('value', params.url);
375             this._form.one(SELECTORS.INPUTH5PURL).set('value', '');
376             this._removeWarnings();
377         }
378     },
380     /**
381      * Set event Listeners for Dialogue content actions.
382      *
383      * @method  _setEventListeners
384      * @private
385      */
386     _setEventListeners: function() {
387         var form = this._form;
388         var permissions = this._getPermissions();
390         form.one(SELECTORS.INPUTSUBMIT).on('click', this._setH5P, this);
392         if (permissions.canUpload) {
393             form.one(SELECTORS.H5PBROWSER).on('click', function() {
394                 this.get('host').showFilepicker('h5p', this._filepickerCallback, this);
395             }, this);
396         }
398         if (permissions.canUploadAndEmbed) {
399             form.one(SELECTORS.INPUTH5PFILE).on('change', function() {
400                 form.one(SELECTORS.INPUTH5PURL).set('value', '');
401                 this._removeWarnings();
402             }, this);
403             form.one(SELECTORS.INPUTH5PURL).on('change', function() {
404                 form.one(SELECTORS.INPUTH5PFILE).set('value', '');
405                 this._removeWarnings();
406             }, this);
407         }
408     },
410     /**
411      * Remove warnings shown in the dialogue.
412      *
413      * @method _removeWarnings
414      * @private
415      */
416     _removeWarnings: function() {
417         var form = this._form;
418         form.one(SELECTORS.URLWARNING).setStyle('display', 'none');
419         form.one(SELECTORS.CONTENTWARNING).setStyle('display', 'none');
420     },
422     /**
423      * Update the h5p in the contenteditable.
425      *
426      * @method _setH5P
427      * @param {EventFacade} e
428      * @private
429      */
430     _setH5P: function(e) {
431         var form = this._form,
432             url = form.one(SELECTORS.INPUTH5PURL).get('value'),
433             h5phtml,
434             host = this.get('host'),
435             h5pfile,
436             permissions = this._getPermissions();
438         if (permissions.canEmbed) {
439             url = form.one(SELECTORS.INPUTH5PURL).get('value');
440         }
442         if (permissions.canUpload) {
443             h5pfile = form.one(SELECTORS.INPUTH5PFILE).get('value');
444         }
446         e.preventDefault();
448         // Check if there are any issues.
449         if (this._updateWarning()) {
450             return;
451         }
453         // Focus on the editor in preparation for inserting the H5P.
454         host.focus();
456         // Add an empty paragraph after new H5P container that can catch the cursor.
457         var addParagraphs = true;
459         // If a H5P placeholder was selected we can destroy it now.
460         if (this._H5PDiv) {
461             this._H5PDiv.remove();
462             addParagraphs = false;
463         }
465         if (url !== '') {
467             host.setSelection(this._currentSelection);
469             if (this._validEmbed(url)) {
470                 var embedtemplate = Y.Handlebars.compile(H5PTEMPLATE);
471                 var regex = /<iframe.*?src="(.*?)".*<\/iframe>/;
472                 var src = url.match(regex)[1];
474                 // In case a local H5P embed code is used we need get the url
475                 // param form the src and decode it.
476                 if (src.startsWith(M.cfg.wwwroot + '/h5p/embed.php')) {
477                     src = decodeURIComponent(src.split("url=")[1]);
478                 }
480                 h5phtml = embedtemplate({
481                     url: src
482                 });
483             } else {
484                 var urltemplate = Y.Handlebars.compile(H5PTEMPLATE);
485                 h5phtml = urltemplate({
486                     url: url
487                 });
488             }
490             this.get('host').insertContentAtFocusPoint(h5phtml);
492             this.markUpdated();
493         } else if (h5pfile !== '') {
495             host.setSelection(this._currentSelection);
497             var options = {};
499             if (form.one(SELECTORS.OPTION_DOWNLOAD_BUTTON).get('checked')) {
500                 options['export'] = '1';
501             }
502             if (form.one(SELECTORS.OPTION_EMBED_BUTTON).get('checked')) {
503                 options.embed = '1';
504             }
505             if (form.one(SELECTORS.OPTION_COPYRIGHT_BUTTON).get('checked')) {
506                 options.copyright = '1';
507             }
509             var params = "";
510             for (var opt in options) {
511                 if (params === "" && (h5pfile.indexOf("?") === -1)) {
512                     params += "?";
513                 } else {
514                     params += "&amp;";
515                 }
516                 params += opt + "=" + options[opt];
517             }
519             var h5ptemplate = Y.Handlebars.compile(H5PTEMPLATE);
521             h5phtml = h5ptemplate({
522                 url: h5pfile + params,
523                 addParagraphs: addParagraphs
524             });
526             this.get('host').insertContentAtFocusPoint(h5phtml);
528             this.markUpdated();
529         }
531         this.getDialogue({
532             focusAfterHide: null
533         }).hide();
534     },
536     /**
537      * Check if this could be a h5p embed.
538      *
539      * @method _validEmbed
540      * @param {String} str
541      * @return {boolean} whether this is a iframe tag.
542      * @private
543      */
544     _validEmbed: function(str) {
545         var pattern = new RegExp('^(<iframe).*(<\\/iframe>)'); // Port and path.
546         return !!pattern.test(str);
547     },
549     /**
550      * Check if this could be a h5p URL.
551      *
552      * @method _validURL
553      * @param {String} str
554      * @return {boolean} whether this is a valid URL.
555      * @private
556      */
557     _validURL: function(str) {
558         var pattern = new RegExp('^(https?:\\/\\/)?' + // Protocol.
559             '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // Domain name.
560             '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address.
561             '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'); // Port and path.
562         return !!pattern.test(str);
563     },
565     /**
566      * Update the url warning.
567      *
568      * @method _updateWarning
569      * @return {boolean} whether a warning should be displayed.
570      * @private
571      */
572     _updateWarning: function() {
573         var form = this._form,
574             state = true,
575             url,
576             h5pfile,
577             permissions = this._getPermissions();
580         if (permissions.canEmbed) {
581             url = form.one(SELECTORS.INPUTH5PURL).get('value');
582             if (url !== '') {
583                 if (this._validURL(url) || this._validEmbed(url)) {
584                     form.one(SELECTORS.URLWARNING).setStyle('display', 'none');
585                     state = false;
586                 } else {
587                     form.one(SELECTORS.URLWARNING).setStyle('display', 'block');
588                     state = true;
589                 }
590                 return state;
591             }
592         }
594         if (permissions.canUpload) {
595             h5pfile = form.one(SELECTORS.INPUTH5PFILE).get('value');
596             if (h5pfile !== '') {
597                 form.one(SELECTORS.CONTENTWARNING).setStyle('display', 'none');
598                 state = false;
599             } else {
600                 form.one(SELECTORS.CONTENTWARNING).setStyle('display', 'block');
601                 state = true;
602             }
603         }
605         return state;
606     }
607 }, {
608     ATTRS: {
609         /**
610          * The allowedmethods of adding h5p content.
611          *
612          * @attribute allowedmethods
613          * @type String
614          */
615         allowedmethods: {
616             value: null
617         }
618     }
619 });