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