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
CommitLineData
297f7e41
BB
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/>.
15
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 */
21
22/**
23 * @module moodle-atto_h5p-button
24 */
25
26/**
27 * Atto h5p content tool.
28 *
29 * @namespace M.atto_h5p
30 * @class Button
31 * @extends M.editor_atto.EditorPlugin
32 */
33
34var CSS = {
59e87e62
BB
35 CONTENTWARNING: 'att_h5p_contentwarning',
36 H5PBROWSER: 'openh5pbrowser',
297f7e41 37 INPUTALT: 'atto_h5p_altentry',
59e87e62 38 INPUTH5PFILE: 'atto_h5p_file',
297f7e41 39 INPUTH5PURL: 'atto_h5p_url',
59e87e62
BB
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',
297f7e41
BB
44 URLWARNING: 'atto_h5p_warning'
45 },
46 SELECTORS = {
59e87e62
BB
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
297f7e41
BB
56 },
57
58 COMPONENTNAME = 'atto_h5p',
59
60 TEMPLATE = '' +
59e87e62
BB
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}}' +
297f7e41
BB
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>' +
59e87e62
BB
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>' +
297f7e41 122 '</div>' +
59e87e62 123 '{{/if}}' +
297f7e41
BB
124 '<div class="text-center">' +
125 '<button class="btn btn-secondary {{CSS.INPUTSUBMIT}}" type="submit">' + '' +
59e87e62 126 '{{get_string "pluginname" component}}</button>' +
297f7e41
BB
127 '</div>' +
128 '</form>',
129
130 H5PTEMPLATE = '' +
fda23bf7
BB
131 '{{#if addParagraphs}}<p><br></p>{{/if}}' +
132 '<div class="h5p-placeholder" contenteditable="false">' +
59e87e62 133 '{{{url}}}' +
fda23bf7
BB
134 '</div>' +
135 '{{#if addParagraphs}}<p><br></p>{{/if}}';
297f7e41
BB
136
137Y.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,
147
148 /**
149 * A reference to the currently open form.
150 *
151 * @param _form
152 * @type Node
153 * @private
154 */
155 _form: null,
156
157 /**
59e87e62 158 * A reference to the currently selected H5P div.
297f7e41
BB
159 *
160 * @param _form
161 * @type Node
162 * @private
163 */
59e87e62
BB
164 _H5PDiv: null,
165
166 /**
167 * Allowed methods of adding H5P.
168 *
169 * @param _allowedmethods
170 * @type String
171 * @private
172 */
173 _allowedmethods: 'none',
297f7e41
BB
174
175 initializer: function() {
59e87e62
BB
176 this._allowedmethods = this.get('allowedmethods');
177 if (this._allowedmethods === 'none') {
297f7e41
BB
178 // Plugin not available here.
179 return;
180 }
297f7e41
BB
181 this.addButton({
182 icon: 'icon',
183 iconComponent: 'atto_h5p',
184 callback: this._displayDialogue,
59e87e62 185 tags: '.h5p-placeholder',
297f7e41
BB
186 tagMatchRequiresAll: false
187 });
188
fda23bf7 189 this.editor.all('.h5p-placeholder').setAttribute('contenteditable', 'false');
59e87e62
BB
190 this.editor.delegate('dblclick', this._handleDblClick, '.h5p-placeholder', this);
191 this.editor.delegate('click', this._handleClick, '.h5p-placeholder', this);
192 },
193
297f7e41
BB
194 /**
195 * Handle a double click on a H5P Placeholder.
196 *
197 * @method _handleDblClick
198 * @private
199 */
200 _handleDblClick: function() {
201 this._displayDialogue();
202 },
203
204 /**
205 * Handle a click on a H5P Placeholder.
206 *
207 * @method _handleClick
208 * @param {EventFacade} e
209 * @private
210 */
211 _handleClick: function(e) {
59e87e62 212 var selection = this.get('host').getSelectionFromNode(e.target);
297f7e41
BB
213 if (this.get('host').getSelection() !== selection) {
214 this.get('host').setSelection(selection);
215 }
216 },
217
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();
297f7e41
BB
227
228 if (this._currentSelection === false) {
229 return;
230 }
59e87e62
BB
231
232 this._getH5PDiv();
233
297f7e41 234 var dialogue = this.getDialogue({
59e87e62 235 headerContent: M.util.get_string('pluginname', COMPONENTNAME),
297f7e41 236 width: 'auto',
59e87e62 237 focusAfterHide: true
297f7e41 238 });
297f7e41
BB
239 // Set the dialogue content, and then show the dialogue.
240 dialogue.set('bodyContent', this._getDialogueContent())
241 .show();
59e87e62 242 M.form.shortforms({formid: this.get('host').get('elementid') + '_atto_h5p_form'});
297f7e41
BB
243 },
244
245 /**
246 * Get the H5P iframe
247 *
248 * @method _resolveH5P
249 * @return {Node} The H5P iframe selected.
250 * @private
251 */
59e87e62
BB
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 },
262
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 };
275
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 }
297f7e41 283 }
59e87e62
BB
284
285 if (this._allowedmethods === 'both' || this._allowedmethods === 'embed') {
286 permissions.canEmbed = true;
287 }
288 return permissions;
297f7e41
BB
289 },
290
291
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() {
59e87e62
BB
301
302 var permissions = this._getPermissions();
303
304 var fileURL,
305 embedURL,
306 optionDownloadButton,
307 optionEmbedButton,
308 optionCopyrightButton,
309 collapseOptions = true;
310
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];
316
317 var parameters = H5PURL.split("?")[1];
318 if (parameters) {
319 if (parameters.match(/export=1/)) {
320 optionDownloadButton = 'checked';
321 collapseOptions = false;
322 }
323
324 if (parameters.match(/embed=1/)) {
325 optionEmbedButton = 'checked';
326 collapseOptions = false;
327 }
328
329 if (parameters.match(/copyright=1/)) {
330 optionCopyrightButton = 'checked';
331 collapseOptions = false;
332 }
333 }
334 } else {
335 embedURL = H5PURL;
336 }
337 }
338
297f7e41
BB
339 var template = Y.Handlebars.compile(TEMPLATE),
340 content = Y.Node.create(template({
341 elementid: this.get('host').get('elementid'),
342 CSS: CSS,
59e87e62
BB
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
297f7e41
BB
353 }));
354
355 this._form = content;
356
59e87e62
BB
357 // Listen to and act on Dialogue content events.
358 this._setEventListeners();
359
360 return content;
361 },
362
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();
297f7e41 377 }
59e87e62 378 },
297f7e41 379
59e87e62
BB
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();
297f7e41 389
59e87e62
BB
390 form.one(SELECTORS.INPUTSUBMIT).on('click', this._setH5P, this);
391
392 if (permissions.canUpload) {
393 form.one(SELECTORS.H5PBROWSER).on('click', function() {
394 this.get('host').showFilepicker('h5p', this._filepickerCallback, this);
395 }, this);
396 }
397
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 }
297f7e41
BB
408 },
409
410 /**
59e87e62
BB
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 },
421
422 /**
423 * Update the h5p in the contenteditable.
424
297f7e41
BB
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,
59e87e62
BB
434 host = this.get('host'),
435 h5pfile,
436 permissions = this._getPermissions();
437
438 if (permissions.canEmbed) {
439 url = form.one(SELECTORS.INPUTH5PURL).get('value');
440 }
441
442 if (permissions.canUpload) {
443 h5pfile = form.one(SELECTORS.INPUTH5PFILE).get('value');
444 }
297f7e41
BB
445
446 e.preventDefault();
447
448 // Check if there are any issues.
449 if (this._updateWarning()) {
450 return;
451 }
452
fda23bf7 453 // Focus on the editor in preparation for inserting the H5P.
297f7e41
BB
454 host.focus();
455
fda23bf7
BB
456 // Add an empty paragraph after new H5P container that can catch the cursor.
457 var addParagraphs = true;
458
59e87e62
BB
459 // If a H5P placeholder was selected we can destroy it now.
460 if (this._H5PDiv) {
461 this._H5PDiv.remove();
fda23bf7 462 addParagraphs = false;
59e87e62 463 }
297f7e41 464
59e87e62 465 if (url !== '') {
297f7e41
BB
466
467 host.setSelection(this._currentSelection);
468
59e87e62
BB
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];
473
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 }
479
480 h5phtml = embedtemplate({
481 url: src
482 });
483 } else {
484 var urltemplate = Y.Handlebars.compile(H5PTEMPLATE);
485 h5phtml = urltemplate({
486 url: url
487 });
488 }
489
490 this.get('host').insertContentAtFocusPoint(h5phtml);
491
492 this.markUpdated();
493 } else if (h5pfile !== '') {
494
495 host.setSelection(this._currentSelection);
496
497 var options = {};
498
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 }
508
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 }
518
519 var h5ptemplate = Y.Handlebars.compile(H5PTEMPLATE);
520
521 h5phtml = h5ptemplate({
fda23bf7
BB
522 url: h5pfile + params,
523 addParagraphs: addParagraphs
297f7e41
BB
524 });
525
526 this.get('host').insertContentAtFocusPoint(h5phtml);
527
528 this.markUpdated();
529 }
530
531 this.getDialogue({
532 focusAfterHide: null
533 }).hide();
534 },
535
59e87e62
BB
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 },
548
297f7e41
BB
549 /**
550 * Check if this could be a h5p URL.
551 *
59e87e62 552 * @method _validURL
297f7e41 553 * @param {String} str
59e87e62 554 * @return {boolean} whether this is a valid URL.
297f7e41
BB
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 },
564
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,
59e87e62
BB
575 url,
576 h5pfile,
577 permissions = this._getPermissions();
578
579
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 }
297f7e41 592 }
59e87e62
BB
593
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 }
604
297f7e41
BB
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});