1 YUI.add('moodle-atto_h5p-button', function (Y, NAME) {
3 // This file is part of Moodle - http://moodle.org/
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.
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/>.
20 * @copyright 2019 Bas Brands <bas@moodle.com>
21 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 * @module moodle-atto_h5p-button
29 * Atto h5p content tool.
31 * @namespace M.atto_h5p
33 * @extends M.editor_atto.EditorPlugin
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'
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
58 COMPONENTNAME = 'atto_h5p',
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}}' +
65 '<div style="display:none" role="alert" class="alert alert-warning mb-1 {{CSS.URLWARNING}}">' +
66 '{{get_string "invalidh5purl" component}}' +
68 '{{#if canUploadAndEmbed}}' +
69 '<div class="mt-2 mb-4 attoh5pinstructions">{{{get_string "instructions" component}}}</div>' +
71 '<div class="mb-4">' +
72 '<label for="{{elementid}}_{{CSS.H5PBROWSER}}">' +
73 '{{#if canUploadAndEmbed}}' +
74 '{{get_string "h5pfileorurl" component}}' +
76 '{{^if canUploadAndEmbed}}' +
78 '{{get_string "h5pfile" component}}' +
81 '{{get_string "h5purl" component}}' +
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"/>' +
89 '<span class="input-group-append">' +
90 '<button class="btn btn-secondary {{CSS.H5PBROWSER}}" type="button">' +
91 '{{get_string "browserepositories" component}}</button>' +
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}}' +
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}}' +
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}}' +
130 '<div class="text-center">' +
131 '<button class="btn btn-secondary {{CSS.INPUTSUBMIT}}" type="submit">' + '' +
132 '{{get_string "pluginname" component}}</button>' +
137 '{{#if addParagraphs}}<p><br></p>{{/if}}' +
138 '<div class="h5p-placeholder" contenteditable="false">' +
141 '{{#if addParagraphs}}<p><br></p>{{/if}}';
143 Y.namespace('M.atto_h5p').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
145 * A reference to the current selection at the time that the dialogue
148 * @property _currentSelection
152 _currentSelection: null,
155 * A reference to the currently open form.
164 * A reference to the currently selected H5P div.
173 * Allowed methods of adding H5P.
175 * @param _allowedmethods
179 _allowedmethods: 'none',
181 initializer: function() {
182 this._allowedmethods = this.get('allowedmethods');
183 if (this._allowedmethods === 'none') {
184 // Plugin not available here.
189 iconComponent: 'atto_h5p',
190 callback: this._displayDialogue,
191 tags: '.h5p-placeholder',
192 tagMatchRequiresAll: false
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);
201 * Handle a double click on a H5P Placeholder.
203 * @method _handleDblClick
206 _handleDblClick: function() {
207 this._displayDialogue();
211 * Handle a click on a H5P Placeholder.
213 * @method _handleClick
214 * @param {EventFacade} e
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);
225 * Display the h5p editing tool.
227 * @method _displayDialogue
230 _displayDialogue: function() {
231 // Store the current selection.
232 this._currentSelection = this.get('host').getSelection();
234 if (this._currentSelection === false) {
240 var dialogue = this.getDialogue({
241 headerContent: M.util.get_string('pluginname', COMPONENTNAME),
245 // Set the dialogue content, and then show the dialogue.
246 dialogue.set('bodyContent', this._getDialogueContent())
248 M.form.shortforms({formid: this.get('host').get('elementid') + '_atto_h5p_form'});
254 * @method _resolveH5P
255 * @return {Node} The H5P iframe selected.
258 _getH5PDiv: function() {
259 var selectednodes = this.get('host').getSelectedNodes();
261 selectednodes.each(function(selNode) {
262 if (selNode.hasClass('h5p-placeholder')) {
266 this._H5PDiv = H5PDiv;
270 * Get the H5P button permissions.
272 * @return {Object} H5P button permissions.
275 _getPermissions: function() {
279 'canUploadAndEmbed': false
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;
291 if (this._allowedmethods === 'both' || this._allowedmethods === 'embed') {
292 permissions.canEmbed = true;
299 * Return the dialogue content for the tool, attaching any required
302 * @method _getDialogueContent
303 * @return {Node} The content to place in the dialogue.
306 _getDialogueContent: function() {
308 var permissions = this._getPermissions();
311 optionDownloadButton,
313 optionCopyrightButton,
314 collapseOptions = true;
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];
324 if (parameters.match(/export=1/)) {
325 optionDownloadButton = 'checked';
326 collapseOptions = false;
329 if (parameters.match(/embed=1/)) {
330 optionEmbedButton = 'checked';
331 collapseOptions = false;
334 if (parameters.match(/copyright=1/)) {
335 optionCopyrightButton = 'checked';
336 collapseOptions = false;
344 var template = Y.Handlebars.compile(TEMPLATE),
345 content = Y.Node.create(template({
346 elementid: this.get('host').get('elementid'),
348 component: COMPONENTNAME,
349 canUpload: permissions.canUpload,
350 canEmbed: permissions.canEmbed,
351 canUploadAndEmbed: permissions.canUploadAndEmbed,
352 collapseOptions: collapseOptions,
354 optionDownloadButton: optionDownloadButton,
355 optionEmbedButton: optionEmbedButton,
356 optionCopyrightButton: optionCopyrightButton
359 this._form = content;
361 // Listen to and act on Dialogue content events.
362 this._setEventListeners();
368 * Update the dialogue after an h5p was selected in the File Picker.
370 * @method _filepickerCallback
371 * @param {object} params The parameters provided by the filepicker
372 * containing information about the h5p.
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();
384 * Set event Listeners for Dialogue content actions.
386 * @method _setEventListeners
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);
401 if (permissions.canUploadAndEmbed) {
402 form.one(SELECTORS.INPUTH5PFILE).on('change', function() {
403 this._removeWarnings();
409 * Remove warnings shown in the dialogue.
411 * @method _removeWarnings
414 _removeWarnings: function() {
415 var form = this._form;
416 form.one(SELECTORS.URLWARNING).setStyle('display', 'none');
417 form.one(SELECTORS.CONTENTWARNING).setStyle('display', 'none');
421 * Update the h5p in the contenteditable.
424 * @param {EventFacade} e
427 _setH5P: function(e) {
428 var form = this._form,
430 host = this.get('host'),
431 h5pfile = form.one(SELECTORS.INPUTH5PFILE).get('value'),
432 permissions = this._getPermissions();
436 // Check if there are any issues.
437 if (this._updateWarning()) {
441 // Focus on the editor in preparation for inserting the H5P.
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.
449 this._H5PDiv.remove();
450 addParagraphs = false;
453 if (h5pfile !== '') {
454 host.setSelection(this._currentSelection);
456 if (h5pfile.startsWith(M.cfg.wwwroot)) {
457 // It's a local file.
459 if (permissions.canUpload) {
461 if (form.one(SELECTORS.OPTION_DOWNLOAD_BUTTON).get('checked')) {
462 options['export'] = '1';
464 if (form.one(SELECTORS.OPTION_EMBED_BUTTON).get('checked')) {
467 if (form.one(SELECTORS.OPTION_COPYRIGHT_BUTTON).get('checked')) {
468 options.copyright = '1';
471 for (var opt in options) {
472 if (params === "" && (h5pfile.indexOf("?") === -1)) {
477 params += opt + "=" + options[opt];
481 var h5ptemplate = Y.Handlebars.compile(H5PTEMPLATE);
483 h5phtml = h5ptemplate({
484 url: h5pfile + params,
485 addParagraphs: addParagraphs
489 var urltemplate = Y.Handlebars.compile(H5PTEMPLATE);
490 h5phtml = urltemplate({
495 host.insertContentAtFocusPoint(h5phtml);
506 * Check if this could be a h5p embed.
508 * @method _validEmbed
509 * @param {String} str
510 * @return {boolean} whether this is a iframe tag.
513 _validEmbed: function(str) {
514 var pattern = new RegExp('^(<iframe).*(<\\/iframe>)'); // Port and path.
515 return !!pattern.test(str);
519 * Check if this could be a h5p URL.
522 * @param {String} str
523 * @return {boolean} whether this is a valid URL.
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);
535 * Update the url warning.
537 * @method _updateWarning
538 * @return {boolean} whether a warning should be displayed.
541 _updateWarning: function() {
542 var form = this._form,
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');
556 form.one(SELECTORS.URLWARNING).setStyle('display', 'block');
560 form.one(SELECTORS.CONTENTWARNING).setStyle('display', 'block');
570 * The allowedmethods of adding h5p content.
572 * @attribute allowedmethods
582 }, '@VERSION@', {"requires": ["moodle-editor_atto-plugin"]});