MDL-68116 atto_h5p: simplify fields to add H5P content
[moodle.git] / lib / editor / atto / plugins / h5p / yui / src / button / js / button.js
blob2906734e42added3cb213b43d1490cbbd8ef942c
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.
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/>.
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         INPUTSUBMIT: 'atto_h5p_urlentrysubmit',
40         OPTION_DOWNLOAD_BUTTON: 'atto_h5p_option_download_button',
41         OPTION_COPYRIGHT_BUTTON: 'atto_h5p_option_copyright_button',
42         OPTION_EMBED_BUTTON: 'atto_h5p_option_embed_button',
43         URLWARNING: 'atto_h5p_warning'
44     },
45     SELECTORS = {
46         CONTENTWARNING: '.' + CSS.CONTENTWARNING,
47         H5PBROWSER: '.' + CSS.H5PBROWSER,
48         INPUTH5PFILE: '.' + CSS.INPUTH5PFILE,
49         INPUTSUBMIT: '.' + CSS.INPUTSUBMIT,
50         OPTION_DOWNLOAD_BUTTON: '.' + CSS.OPTION_DOWNLOAD_BUTTON,
51         OPTION_COPYRIGHT_BUTTON: '.' + CSS.OPTION_COPYRIGHT_BUTTON,
52         OPTION_EMBED_BUTTON: '.' + CSS.OPTION_EMBED_BUTTON,
53         URLWARNING: '.' + CSS.URLWARNING
54     },
56     COMPONENTNAME = 'atto_h5p',
58     TEMPLATE = '' +
59             '<form class="atto_form mform" id="{{elementid}}_atto_h5p_form">' +
60                 '<div style="display:none" role="alert" class="alert alert-warning mb-1 {{CSS.CONTENTWARNING}}">' +
61                     '{{get_string "noh5pcontent" component}}' +
62                 '</div>' +
63                 '<div style="display:none" role="alert" class="alert alert-warning mb-1 {{CSS.URLWARNING}}">' +
64                     '{{get_string "invalidh5purl" component}}' +
65                 '</div>' +
66                 '{{#if canUploadAndEmbed}}' +
67                     '<div class="mt-2 mb-4 attoh5pinstructions">{{{get_string "instructions" component}}}</div>' +
68                 '{{/if}}' +
69                 '<div class="mb-4">' +
70                     '<label for="{{elementid}}_{{CSS.H5PBROWSER}}">' +
71                         '{{#if canUploadAndEmbed}}' +
72                             '{{get_string "h5pfileorurl" component}}' +
73                         '{{/if}}' +
74                         '{{^if canUploadAndEmbed}}' +
75                             '{{#if canUpload}}' +
76                                 '{{get_string "h5pfile" component}}' +
77                             '{{/if}}' +
78                             '{{#if canEmbed}}' +
79                                 '{{get_string "h5purl" component}}' +
80                             '{{/if}}' +
81                         '{{/if}}' +
82                     '</label>' +
83                     '<div class="input-group input-append w-100">' +
84                         '<input class="form-control {{CSS.INPUTH5PFILE}}" type="url" value="{{fileURL}}" ' +
85                         'id="{{elementid}}_{{CSS.INPUTH5PFILE}}" data-region="h5pfile" size="32"/>' +
86                         '{{#if canUpload}}' +
87                             '<span class="input-group-append">' +
88                                 '<button class="btn btn-secondary {{CSS.H5PBROWSER}}" type="button">' +
89                                 '{{get_string "browserepositories" component}}</button>' +
90                             '</span>' +
91                         '{{/if}}' +
92                     '</div>' +
93                     '{{#if canUpload}}' +
94                         '<fieldset class="collapsible {{#if collapseOptions}}collapsed{{/if}}" id="{{elementid}}_h5poptions">' +
95                             '<legend class="ftoggler">{{get_string "h5poptions" component}}</legend>' +
96                             '<div class="fcontainer">' +
97                                 '<div class="form-check">' +
98                                     '<input type="checkbox" {{optionDownloadButton}} ' +
99                                     'class="form-check-input {{CSS.OPTION_DOWNLOAD_BUTTON}}"' +
100                                     'aria-label="{{get_string "downloadbutton" component}}" ' +
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                                     'aria-label="{{get_string "embedbutton" component}}" ' +
110                                         'id="{{elementid}}_h5p-option-embed-button"/>' +
111                                     '<label class="form-check-label" for="{{elementid}}_h5p-option-embed-button">' +
112                                     '{{get_string "embedbutton" component}}' +
113                                     '</label>' +
114                                 '</div>' +
115                                 '<div class="form-check mb-2">' +
116                                     '<input type="checkbox" {{optionCopyrightButton}} ' +
117                                     'class="form-check-input {{CSS.OPTION_COPYRIGHT_BUTTON}}" ' +
118                                     'aria-label="{{get_string "copyrightbutton" component}}" ' +
119                                         'id="{{elementid}}_h5p-option-copyright-button"/>' +
120                                     '<label class="form-check-label" for="{{elementid}}_h5p-option-copyright-button">' +
121                                     '{{get_string "copyrightbutton" component}}' +
122                                     '</label>' +
123                                 '</div>' +
124                             '</div>' +
125                         '</fieldset>' +
126                     '{{/if}}' +
127                 '</div>' +
128                 '<div class="text-center">' +
129                 '<button class="btn btn-secondary {{CSS.INPUTSUBMIT}}" type="submit">' + '' +
130                     '{{get_string "pluginname" component}}</button>' +
131                 '</div>' +
132             '</form>',
134         H5PTEMPLATE = '' +
135             '{{#if addParagraphs}}<p><br></p>{{/if}}' +
136             '<div class="h5p-placeholder" contenteditable="false">' +
137                 '{{{url}}}' +
138             '</div>' +
139             '{{#if addParagraphs}}<p><br></p>{{/if}}';
141 Y.namespace('M.atto_h5p').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
142     /**
143      * A reference to the current selection at the time that the dialogue
144      * was opened.
145      *
146      * @property _currentSelection
147      * @type Range
148      * @private
149      */
150     _currentSelection: null,
152     /**
153      * A reference to the currently open form.
154      *
155      * @param _form
156      * @type Node
157      * @private
158      */
159     _form: null,
161     /**
162      * A reference to the currently selected H5P div.
163      *
164      * @param _form
165      * @type Node
166      * @private
167      */
168     _H5PDiv: null,
170     /**
171      * Allowed methods of adding H5P.
172      *
173      * @param _allowedmethods
174      * @type String
175      * @private
176      */
177     _allowedmethods: 'none',
179     initializer: function() {
180         this._allowedmethods = this.get('allowedmethods');
181         if (this._allowedmethods === 'none') {
182             // Plugin not available here.
183             return;
184         }
185         this.addButton({
186             icon: 'icon',
187             iconComponent: 'atto_h5p',
188             callback: this._displayDialogue,
189             tags: '.h5p-placeholder',
190             tagMatchRequiresAll: false
191         });
193         this.editor.all('.h5p-placeholder').setAttribute('contenteditable', 'false');
194         this.editor.delegate('dblclick', this._handleDblClick, '.h5p-placeholder', this);
195         this.editor.delegate('click', this._handleClick, '.h5p-placeholder', this);
196     },
198     /**
199      * Handle a double click on a H5P Placeholder.
200      *
201      * @method _handleDblClick
202      * @private
203      */
204     _handleDblClick: function() {
205         this._displayDialogue();
206     },
208     /**
209      * Handle a click on a H5P Placeholder.
210      *
211      * @method _handleClick
212      * @param {EventFacade} e
213      * @private
214      */
215     _handleClick: function(e) {
216         var selection = this.get('host').getSelectionFromNode(e.target);
217         if (this.get('host').getSelection() !== selection) {
218             this.get('host').setSelection(selection);
219         }
220     },
222     /**
223      * Display the h5p editing tool.
224      *
225      * @method _displayDialogue
226      * @private
227      */
228     _displayDialogue: function() {
229         // Store the current selection.
230         this._currentSelection = this.get('host').getSelection();
232         if (this._currentSelection === false) {
233             return;
234         }
236         this._getH5PDiv();
238         var dialogue = this.getDialogue({
239             headerContent: M.util.get_string('pluginname', COMPONENTNAME),
240             width: 'auto',
241             focusAfterHide: true
242         });
243         // Set the dialogue content, and then show the dialogue.
244         dialogue.set('bodyContent', this._getDialogueContent())
245             .show();
246         M.form.shortforms({formid: this.get('host').get('elementid') + '_atto_h5p_form'});
247     },
249     /**
250      * Get the H5P iframe
251      *
252      * @method _resolveH5P
253      * @return {Node} The H5P iframe selected.
254      * @private
255      */
256     _getH5PDiv: function() {
257         var selectednodes = this.get('host').getSelectedNodes();
258         var H5PDiv = null;
259         selectednodes.each(function(selNode) {
260             if (selNode.hasClass('h5p-placeholder')) {
261                 H5PDiv = selNode;
262             }
263         });
264         this._H5PDiv = H5PDiv;
265     },
267     /**
268      * Get the H5P button permissions.
269      *
270      * @return {Object} H5P button permissions.
271      * @private
272      */
273     _getPermissions: function() {
274         var permissions = {
275             'canEmbed': false,
276             'canUpload': false,
277             'canUploadAndEmbed': false
278         };
280         if (this.get('host').canShowFilepicker('h5p')) {
281             if (this._allowedmethods === 'both') {
282                 permissions.canUploadAndEmbed = true;
283                 permissions.canUpload = true;
284             } else if (this._allowedmethods === 'upload') {
285                 permissions.canUpload = true;
286             }
287         }
289         if (this._allowedmethods === 'both' || this._allowedmethods === 'embed') {
290             permissions.canEmbed = true;
291         }
292         return permissions;
293     },
296     /**
297      * Return the dialogue content for the tool, attaching any required
298      * events.
299      *
300      * @method _getDialogueContent
301      * @return {Node} The content to place in the dialogue.
302      * @private
303      */
304     _getDialogueContent: function() {
306         var permissions = this._getPermissions();
308         var fileURL,
309             optionDownloadButton,
310             optionEmbedButton,
311             optionCopyrightButton,
312             collapseOptions = true;
314         if (this._H5PDiv) {
315             var H5PURL = this._H5PDiv.get('innerHTML');
316             var fileBaseUrl = M.cfg.wwwroot + '/draftfile.php';
317             if (fileBaseUrl == H5PURL.substring(0, fileBaseUrl.length)) {
318                 fileURL = H5PURL.split("?")[0];
320                 var parameters = H5PURL.split("?")[1];
321                 if (parameters) {
322                     if (parameters.match(/export=1/)) {
323                         optionDownloadButton = 'checked';
324                         collapseOptions = false;
325                     }
327                     if (parameters.match(/embed=1/)) {
328                         optionEmbedButton = 'checked';
329                         collapseOptions = false;
330                     }
332                     if (parameters.match(/copyright=1/)) {
333                         optionCopyrightButton = 'checked';
334                         collapseOptions = false;
335                     }
336                 }
337             } else {
338                 fileURL = H5PURL;
339             }
340         }
342         var template = Y.Handlebars.compile(TEMPLATE),
343             content = Y.Node.create(template({
344                 elementid: this.get('host').get('elementid'),
345                 CSS: CSS,
346                 component: COMPONENTNAME,
347                 canUpload: permissions.canUpload,
348                 canEmbed: permissions.canEmbed,
349                 canUploadAndEmbed: permissions.canUploadAndEmbed,
350                 collapseOptions: collapseOptions,
351                 fileURL: fileURL,
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._removeWarnings();
378         }
379     },
381     /**
382      * Set event Listeners for Dialogue content actions.
383      *
384      * @method  _setEventListeners
385      * @private
386      */
387     _setEventListeners: function() {
388         var form = this._form;
389         var permissions = this._getPermissions();
391         form.one(SELECTORS.INPUTSUBMIT).on('click', this._setH5P, this);
393         if (permissions.canUpload) {
394             form.one(SELECTORS.H5PBROWSER).on('click', function() {
395                 this.get('host').showFilepicker('h5p', this._filepickerCallback, this);
396             }, this);
397         }
399         if (permissions.canUploadAndEmbed) {
400             form.one(SELECTORS.INPUTH5PFILE).on('change', function() {
401                 this._removeWarnings();
402             }, this);
403         }
404     },
406     /**
407      * Remove warnings shown in the dialogue.
408      *
409      * @method _removeWarnings
410      * @private
411      */
412     _removeWarnings: function() {
413         var form = this._form;
414         form.one(SELECTORS.URLWARNING).setStyle('display', 'none');
415         form.one(SELECTORS.CONTENTWARNING).setStyle('display', 'none');
416     },
418     /**
419      * Update the h5p in the contenteditable.
420      *
421      * @method _setH5P
422      * @param {EventFacade} e
423      * @private
424      */
425     _setH5P: function(e) {
426         var form = this._form,
427             h5phtml,
428             host = this.get('host'),
429             h5pfile = form.one(SELECTORS.INPUTH5PFILE).get('value'),
430             permissions = this._getPermissions();
432         e.preventDefault();
434         // Check if there are any issues.
435         if (this._updateWarning()) {
436             return;
437         }
439         // Focus on the editor in preparation for inserting the H5P.
440         host.focus();
442         // Add an empty paragraph after new H5P container that can catch the cursor.
443         var addParagraphs = true;
445         // If a H5P placeholder was selected we can destroy it now.
446         if (this._H5PDiv) {
447             this._H5PDiv.remove();
448             addParagraphs = false;
449         }
451         if (h5pfile !== '') {
452             host.setSelection(this._currentSelection);
454             if (h5pfile.startsWith(M.cfg.wwwroot)) {
455                 // It's a local file.
456                 var params = '';
457                 if (permissions.canUpload) {
458                     var options = {};
459                     if (form.one(SELECTORS.OPTION_DOWNLOAD_BUTTON).get('checked')) {
460                         options['export'] = '1';
461                     }
462                     if (form.one(SELECTORS.OPTION_EMBED_BUTTON).get('checked')) {
463                         options.embed = '1';
464                     }
465                     if (form.one(SELECTORS.OPTION_COPYRIGHT_BUTTON).get('checked')) {
466                         options.copyright = '1';
467                     }
469                     for (var opt in options) {
470                         if (params === "" && (h5pfile.indexOf("?") === -1)) {
471                             params += "?";
472                         } else {
473                             params += "&amp;";
474                         }
475                         params += opt + "=" + options[opt];
476                     }
477                 }
479                 var h5ptemplate = Y.Handlebars.compile(H5PTEMPLATE);
481                 h5phtml = h5ptemplate({
482                     url: h5pfile + params,
483                     addParagraphs: addParagraphs
484                 });
485             } else {
486                 // It's a URL.
487                 var urltemplate = Y.Handlebars.compile(H5PTEMPLATE);
488                 h5phtml = urltemplate({
489                     url: h5pfile
490                 });
491             }
493             host.insertContentAtFocusPoint(h5phtml);
495             this.markUpdated();
496         }
498         this.getDialogue({
499             focusAfterHide: null
500         }).hide();
501     },
503     /**
504      * Check if this could be a h5p embed.
505      *
506      * @method _validEmbed
507      * @param {String} str
508      * @return {boolean} whether this is a iframe tag.
509      * @private
510      */
511     _validEmbed: function(str) {
512         var pattern = new RegExp('^(<iframe).*(<\\/iframe>)'); // Port and path.
513         return !!pattern.test(str);
514     },
516     /**
517      * Check if this could be a h5p URL.
518      *
519      * @method _validURL
520      * @param {String} str
521      * @return {boolean} whether this is a valid URL.
522      * @private
523      */
524     _validURL: function(str) {
525         var pattern = new RegExp('^(https?:\\/\\/)?' + // Protocol.
526             '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // Domain name.
527             '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address.
528             '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'); // Port and path.
529         return !!pattern.test(str);
530     },
532     /**
533      * Update the url warning.
534      *
535      * @method _updateWarning
536      * @return {boolean} whether a warning should be displayed.
537      * @private
538      */
539     _updateWarning: function() {
540         var form = this._form,
541             state = true,
542             h5pfile,
543             permissions = this._getPermissions();
545         if (permissions.canUpload || permissions.canEmbed) {
546             h5pfile = form.one(SELECTORS.INPUTH5PFILE).get('value');
547             if (h5pfile !== '') {
548                 form.one(SELECTORS.CONTENTWARNING).setStyle('display', 'none');
549                 if (h5pfile.startsWith(M.cfg.wwwroot) || this._validURL(h5pfile)) {
550                     // Only external URLs have to be validated.
551                     form.one(SELECTORS.URLWARNING).setStyle('display', 'none');
552                     state = false;
553                 } else {
554                     form.one(SELECTORS.URLWARNING).setStyle('display', 'block');
555                     state = true;
556                 }
557             } else {
558                 form.one(SELECTORS.CONTENTWARNING).setStyle('display', 'block');
559                 state = true;
560             }
561         }
563         return state;
564     }
565 }, {
566     ATTRS: {
567         /**
568          * The allowedmethods of adding h5p content.
569          *
570          * @attribute allowedmethods
571          * @type String
572          */
573         allowedmethods: {
574             value: null
575         }
576     }