MDL-66388 h5p: add h5p atto button

This commit is contained in:
Bas Brands 2019-08-20 16:22:45 +02:00
parent 1c3efe48f8
commit 297f7e411c
24 changed files with 1446 additions and 3 deletions

View File

@ -9,6 +9,7 @@ cache/stores/mongodb/MongoDB/
enrol/lti/ims-blti/
filter/algebra/AlgParser.pm
filter/tex/mimetex.*
lib/editor/atto/plugins/h5p/js/h5p-resizer.js
lib/editor/atto/plugins/html/yui/src/codemirror/
lib/editor/atto/plugins/html/yui/src/beautify/
lib/editor/atto/yui/src/rangy/js/*.*

View File

@ -10,6 +10,7 @@ cache/stores/mongodb/MongoDB/
enrol/lti/ims-blti/
filter/algebra/AlgParser.pm
filter/tex/mimetex.*
lib/editor/atto/plugins/h5p/js/h5p-resizer.js
lib/editor/atto/plugins/html/yui/src/codemirror/
lib/editor/atto/plugins/html/yui/src/beautify/
lib/editor/atto/yui/src/rangy/js/*.*

View File

@ -1687,7 +1687,7 @@ class core_plugin_manager {
'equation', 'fontcolor', 'html', 'image', 'indent', 'italic',
'link', 'managefiles', 'media', 'noautolink', 'orderedlist',
'recordrtc', 'rtl', 'strike', 'subscript', 'superscript', 'table',
'title', 'underline', 'undo', 'unorderedlist'
'title', 'underline', 'undo', 'unorderedlist', 'h5p'
),
'assignment' => array(

View File

@ -103,5 +103,28 @@ function xmldb_editor_atto_upgrade($oldversion) {
// Automatically generated Moodle v3.7.0 release upgrade line.
// Put any upgrade step following this.
if ($oldversion < 2019090900) {
$toolbar = get_config('editor_atto', 'toolbar');
if (strpos($toolbar, 'h5p') === false) {
$glue = "\r\n";
if (strpos($toolbar, $glue) === false) {
$glue = "\n";
}
$groups = explode($glue, $toolbar);
// Try to put h5p in the files group.
foreach ($groups as $i => $group) {
$parts = explode('=', $group);
if (trim($parts[0]) == 'files') {
$groups[$i] = 'files = ' . trim($parts[1]) . ', h5p';
// Update config variable.
$toolbar = implode($glue, $groups);
set_config('toolbar', $toolbar, 'editor_atto');
}
}
}
// Atto editor savepoint reached.
upgrade_plugin_savepoint(true, 2019090900, 'editor', 'atto');
}
return true;
}

View File

@ -0,0 +1,46 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Privacy Subsystem implementation for atto_h5p.
*
* @package atto_h5p
* @copyright 2019 Bas Brands <bas@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace atto_h5p\privacy;
defined('MOODLE_INTERNAL') || die();
/**
* Privacy Subsystem for atto_h5p implementing null_provider.
*
* @copyright 2019 Bas Brands <bas@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class provider implements \core_privacy\local\metadata\null_provider {
/**
* Get the language string identifier with the component's language
* file to explain why this plugin stores no data.
*
* @return string
*/
public static function get_reason() : string {
return 'privacy:metadata';
}
}

View File

@ -0,0 +1,35 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* H5P Atto button capabilities.
*
* @package atto_h5p
* @copyright 2019 Bas Brands <bas@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$capabilities = [
'atto/h5p:addembed' => [
'captype' => 'write',
'contextlevel' => CONTEXT_MODULE,
'archetypes' => [
'editingteacher' => CAP_ALLOW,
],
]
];

View File

@ -0,0 +1,131 @@
// H5P iframe Resizer
(function () {
if (!window.postMessage || !window.addEventListener || window.h5pResizerInitialized) {
return; // Not supported
}
window.h5pResizerInitialized = true;
// Map actions to handlers
var actionHandlers = {};
/**
* Prepare iframe resize.
*
* @private
* @param {Object} iframe Element
* @param {Object} data Payload
* @param {Function} respond Send a response to the iframe
*/
actionHandlers.hello = function (iframe, data, respond) {
// Make iframe responsive
iframe.style.width = '100%';
// Bugfix for Chrome: Force update of iframe width. If this is not done the
// document size may not be updated before the content resizes.
iframe.getBoundingClientRect();
// Tell iframe that it needs to resize when our window resizes
var resize = function () {
if (iframe.contentWindow) {
// Limit resize calls to avoid flickering
respond('resize');
}
else {
// Frame is gone, unregister.
window.removeEventListener('resize', resize);
}
};
window.addEventListener('resize', resize, false);
// Respond to let the iframe know we can resize it
respond('hello');
};
/**
* Prepare iframe resize.
*
* @private
* @param {Object} iframe Element
* @param {Object} data Payload
* @param {Function} respond Send a response to the iframe
*/
actionHandlers.prepareResize = function (iframe, data, respond) {
// Do not resize unless page and scrolling differs
if (iframe.clientHeight !== data.scrollHeight ||
data.scrollHeight !== data.clientHeight) {
// Reset iframe height, in case content has shrinked.
iframe.style.height = data.clientHeight + 'px';
respond('resizePrepared');
}
};
/**
* Resize parent and iframe to desired height.
*
* @private
* @param {Object} iframe Element
* @param {Object} data Payload
* @param {Function} respond Send a response to the iframe
*/
actionHandlers.resize = function (iframe, data) {
// Resize iframe so all content is visible. Use scrollHeight to make sure we get everything
iframe.style.height = data.scrollHeight + 'px';
};
/**
* Keyup event handler. Exits full screen on escape.
*
* @param {Event} event
*/
var escape = function (event) {
if (event.keyCode === 27) {
exitFullScreen();
}
};
// Listen for messages from iframes
window.addEventListener('message', function receiveMessage(event) {
if (event.data.context !== 'h5p') {
return; // Only handle h5p requests.
}
// Find out who sent the message
var iframe, iframes = document.getElementsByTagName('iframe');
for (var i = 0; i < iframes.length; i++) {
if (iframes[i].contentWindow === event.source) {
iframe = iframes[i];
break;
}
}
if (!iframe) {
return; // Cannot find sender
}
// Find action handler handler
if (actionHandlers[event.data.action]) {
actionHandlers[event.data.action](iframe, event.data, function respond(action, data) {
if (data === undefined) {
data = {};
}
data.action = action;
data.context = 'h5p';
event.source.postMessage(data, event.origin);
});
}
}, false);
// Let h5p iframes know we're ready!
var iframes = document.getElementsByTagName('iframe');
var ready = {
context: 'h5p',
action: 'ready'
};
for (var i = 0; i < iframes.length; i++) {
if (iframes[i].src.indexOf('h5p') !== -1) {
iframes[i].contentWindow.postMessage(ready, '*');
}
}
})();

View File

@ -0,0 +1,9 @@
The H5P resizer JS.
to update:
Downloaded last release from: https://github.com/h5p/h5p-php-library/releases
Import
- In the downloaded h5p-php-library copy js/h5p-resizer.js into lib/editor/atto/plugins/h5p/js

View File

@ -0,0 +1,31 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Strings for component 'atto_h5p', language 'en'.
*
* @package atto_h5p
* @copyright 2019 Bas Brands <bas@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['enterurl'] = 'Enter URL';
$string['h5pproperties'] = 'H5P properties';
$string['invalidh5purl'] = 'Invalid URL';
$string['pluginname'] = 'Insert H5P';
$string['privacy:metadata'] = 'The atto_h5p plugin does not store any personal data.';
$string['h5p:addembed'] = 'Add embedded H5P';
$string['saveh5p'] = 'Save H5P';

View File

@ -0,0 +1,67 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Atto text editor integration version file.
*
* @package atto_h5p
* @copyright 2019 Bas Brands <bas@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Set params for this button.
*
* @param string $elementid
* @param stdClass $options - the options for the editor, including the context.
* @param stdClass $fpoptions - unused.
*/
function atto_h5p_params_for_js($elementid, $options, $fpoptions) {
$context = $options['context'];
if (!$context) {
$context = context_system::instance();
}
$addembed = has_capability('atto/h5p:addembed', $context);
$allowedmethods = 'none';
if ($addembed) {
$allowedmethods = 'embed';
}
$params = ['allowedmethods' => $allowedmethods];
return $params;
}
/**
* Initialise the strings required for js
*/
function atto_h5p_strings_for_js() {
global $PAGE;
$strings = array(
'saveh5p',
'h5pproperties',
'enterurl',
'invalidh5purl'
);
$PAGE->requires->strings_for_js($strings, 'atto_h5p');
$PAGE->requires->js(new moodle_url('/lib/editor/atto/plugins/h5p/js/h5p-resizer.js'));
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 345 150" style="enable-background:new 0 0 345 150;" xml:space="preserve">
<g>
<path d="M325.7,14.7C317.6,6.9,305.3,3,289,3h-43.5H234v31h-66l-5.4,22.2c4.5-2.1,10.9-4.2,15.3-5.3c4.4-1.1,8.8-0.9,13.1-0.9
c14.6,0,26.5,4.5,35.6,13.3c9.1,8.8,13.6,20,13.6,33.4c0,9.4-2.3,18.5-7,27.2s-11.3,15.4-19.9,20c-3.1,1.6-6.5,3.1-10.2,4.1h42.4
H259V95h25c18.2,0,31.7-4.2,40.6-12.5s13.3-19.9,13.3-34.6C337.9,33.6,333.8,22.5,325.7,14.7z M288.7,60.6c-3.5,3-9.6,4.4-18.3,4.4
H259V33h13.2c8.4,0,14.2,1.5,17.2,4.7c3.1,3.2,4.6,6.9,4.6,11.5C294,53.9,292.2,57.6,288.7,60.6z"/>
<path d="M176.5,76.3c-7.9,0-14.7,4.6-18,11.2L119,81.9L136.8,3h-23.6H101v62H51V3H7v145h44V95h50v53h12.2h42
c-6.7-2-12.5-4.6-17.2-8.1c-4.8-3.6-8.7-7.7-11.7-12.3c-3-4.6-5.3-9.7-7.3-16.5l39.6-5.7c3.3,6.6,10.1,11.1,17.9,11.1
c11.1,0,20.1-9,20.1-20.1S187.5,76.3,176.5,76.3z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,17 @@
.attoh5poverlay {
display: none;
}
.editor_atto_content_wrap .attoh5poverlay {
display: block;
position: absolute;
cursor: pointer;
top: 0;
left: 0;
bottom: 0;
width: 100%;
height: 100%;
background: url([[pix:atto_h5p|icon]]) center center / 100px auto no-repeat #adb5bd;
}
.h5p-embed-placeholder .attoh5poverlay + br {
display: none;
}

View File

@ -0,0 +1,56 @@
@editor @editor_atto @atto @atto_h5p @_switch_iframe
Feature: Add h5ps to Atto
To write rich text - I need to add h5ps.
Background:
Given the following "courses" exist:
| shortname | fullname |
| C1 | Course 1 |
And the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "activities" exist:
| activity | name | intro | introformat | course | content | contentformat | idnumber |
| page | PageName1 | PageDesc1 | 1 | C1 | H5Ptest | 1 | 1 |
@javascript
Scenario: Insert an embedded h5p
Given I log in as "admin"
And I am on "Course 1" course homepage
And I follow "PageName1"
And I navigate to "Edit settings" in current page administration
And I click on "Insert H5P" "button" in the "#fitem_id_page" "css_element"
And I set the field "Enter URL" to "https://h5p.org/h5p/embed/576651"
And I click on "Save H5P" "button" in the "H5P properties" "dialogue"
And I wait until the page is ready
When I click on "Save and display" "button"
And I switch to "h5pcontent" iframe
Then ".h5p-iframe" "css_element" should exist
@javascript
Scenario: Test an invalid url
Given I log in as "admin"
And I am on "Course 1" course homepage
And I follow "PageName1"
And I navigate to "Edit settings" in current page administration
And I click on "Insert H5P" "button" in the "#fitem_id_page" "css_element"
And I set the field "Enter URL" to "ftp://h5p.org/h5p/embed/576651"
And I click on "Save H5P" "button" in the "H5P properties" "dialogue"
And I wait until the page is ready
Then I should see "Invalid URL" in the "H5P properties" "dialogue"
@javascript
Scenario: No embed h5p capabilities
Given I log in as "admin"
And I set the following system permissions of "Teacher" role:
| capability | permission |
| atto/h5p:addembed | Prohibit |
And I log out
And I log in as "teacher1"
And I am on "Course 1" course homepage
And I follow "PageName1"
And I navigate to "Edit settings" in current page administration
Then "Insert H5P" "button" should not exist

View File

@ -0,0 +1,10 @@
<?xml version="1.0"?>
<libraries>
<library>
<location>js/h5p-resizer.js</location>
<name>H5P Resizer</name>
<license>GPL-3.0</license>
<version>1.23.1</version>
<licenseversion></licenseversion>
</library>
</libraries>

View File

@ -0,0 +1,29 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Atto text editor integration version file.
*
* @package atto_h5p
* @copyright 2019 Bas Brands <bas@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2019081900; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2019051100; // Requires this Moodle version.
$plugin->component = 'atto_h5p'; // Full name of the plugin (used for diagnostics).

View File

@ -0,0 +1,320 @@
YUI.add('moodle-atto_h5p-button', function (Y, NAME) {
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/*
* @package atto_h5p
* @copyright 2019 Bas Brands <bas@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* @module moodle-atto_h5p-button
*/
/**
* Atto h5p content tool.
*
* @namespace M.atto_h5p
* @class Button
* @extends M.editor_atto.EditorPlugin
*/
var CSS = {
INPUTALT: 'atto_h5p_altentry',
INPUTSUBMIT: 'atto_h5p_urlentrysubmit',
INPUTH5PURL: 'atto_h5p_url',
URLWARNING: 'atto_h5p_warning'
},
SELECTORS = {
INPUTH5PURL: '.' + CSS.INPUTH5PURL
},
COMPONENTNAME = 'atto_h5p',
TEMPLATE = '' +
'<form class="atto_form">' +
'<div class="mb-4">' +
'<label for="{{elementid}}_{{CSS.INPUTH5PURL}}">{{get_string "enterurl" component}}</label>' +
'<div style="display:none" role="alert" class="alert alert-warning mb-1 {{CSS.URLWARNING}}">' +
'{{get_string "invalidh5purl" component}}' +
'</div>' +
'<input class="form-control fullwidth {{CSS.INPUTH5PURL}}" type="url" ' +
'id="{{elementid}}_{{CSS.INPUTH5PURL}}" size="32"/>' +
'</div>' +
'<div class="text-center">' +
'<button class="btn btn-secondary {{CSS.INPUTSUBMIT}}" type="submit">' + '' +
'{{get_string "saveh5p" component}}</button>' +
'</div>' +
'</form>',
H5PTEMPLATE = '' +
'<div class="position-relative h5p-embed-placeholder">' +
'<div class="attoh5poverlay"></div>' +
'<iframe id="h5pcontent" class="h5pcontent" src="{{url}}/embed" ' +
'width="100%" height="637" frameborder="0"' +
'allowfullscreen="{{allowfullscreen}}" allowmedia="{{allowmedia}}">' +
'</iframe>' +
'<script src="' + M.cfg.wwwroot + '/lib/editor/atto/plugins/h5p/js/h5p-resizer.js"' +
'charset="UTF-8"></script>' +
'</div>' +
'</div>' +
'<p><br></p>';
Y.namespace('M.atto_h5p').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
/**
* A reference to the current selection at the time that the dialogue
* was opened.
*
* @property _currentSelection
* @type Range
* @private
*/
_currentSelection: null,
/**
* A reference to the currently open form.
*
* @param _form
* @type Node
* @private
*/
_form: null,
/**
* A reference to the currently selected H5P placeholder.
*
* @param _form
* @type Node
* @private
*/
_placeholderH5P: null,
initializer: function() {
var allowedmethods = this.get('allowedmethods');
if (allowedmethods !== 'embed') {
// Plugin not available here.
return;
}
this.addButton({
icon: 'icon',
iconComponent: 'atto_h5p',
callback: this._displayDialogue,
tags: '.attoh5poverlay',
tagMatchRequiresAll: false
});
this.editor.delegate('dblclick', this._handleDblClick, '.attoh5poverlay', this);
this.editor.delegate('click', this._handleClick, '.attoh5poverlay', this);
},
/**
* Handle a double click on a H5P Placeholder.
*
* @method _handleDblClick
* @private
*/
_handleDblClick: function() {
this._displayDialogue();
},
/**
* Handle a click on a H5P Placeholder.
*
* @method _handleClick
* @param {EventFacade} e
* @private
*/
_handleClick: function(e) {
var h5pplaceholder = e.target;
var selection = this.get('host').getSelectionFromNode(h5pplaceholder);
if (this.get('host').getSelection() !== selection) {
this.get('host').setSelection(selection);
}
},
/**
* Display the h5p editing tool.
*
* @method _displayDialogue
* @private
*/
_displayDialogue: function() {
// Store the current selection.
this._currentSelection = this.get('host').getSelection();
this._placeholderH5P = this._getH5PIframe();
if (this._currentSelection === false) {
return;
}
var dialogue = this.getDialogue({
headerContent: M.util.get_string('h5pproperties', COMPONENTNAME),
width: 'auto',
focusAfterHide: true,
focusOnShowSelector: SELECTORS.INPUTH5PURL
});
// Set the dialogue content, and then show the dialogue.
dialogue.set('bodyContent', this._getDialogueContent())
.show();
},
/**
* Get the H5P iframe
*
* @method _resolveH5P
* @return {Node} The H5P iframe selected.
* @private
*/
_getH5PIframe: function() {
var selectednode = this.get('host').getSelectionParentNode();
if (!selectednode) {
return;
}
return Y.one(selectednode).one('iframe.h5pcontent');
},
/**
* Return the dialogue content for the tool, attaching any required
* events.
*
* @method _getDialogueContent
* @return {Node} The content to place in the dialogue.
* @private
*/
_getDialogueContent: function() {
var template = Y.Handlebars.compile(TEMPLATE),
content = Y.Node.create(template({
elementid: this.get('host').get('elementid'),
CSS: CSS,
component: COMPONENTNAME
}));
this._form = content;
if (this._placeholderH5P) {
var oldurl = this._placeholderH5P.getAttribute('src');
this._form.one(SELECTORS.INPUTH5PURL).setAttribute('value', oldurl);
}
this._form.one('.' + CSS.INPUTSUBMIT).on('click', this._setH5P, this);
return content;
},
/**
* Set the h5p in the contenteditable.
*
* @method _setH5P
* @param {EventFacade} e
* @private
*/
_setH5P: function(e) {
var form = this._form,
url = form.one(SELECTORS.INPUTH5PURL).get('value'),
h5phtml,
host = this.get('host');
e.preventDefault();
// Check if there are any issues.
if (this._updateWarning()) {
return;
}
// Focus on the editor in preparation for inserting the h5p.
host.focus();
// If a H5P placeholder was selected we only update the placeholder.
if (this._placeholderH5P) {
this._placeholderH5P.setAttribute('src', url);
} else if (url !== '') {
host.setSelection(this._currentSelection);
var template = Y.Handlebars.compile(H5PTEMPLATE);
h5phtml = template({
url: url,
allowfullscreen: 'allowfullscreen',
allowmedia: 'geolocation *; microphone *; camera *; midi *; encrypted-media *'
});
this.get('host').insertContentAtFocusPoint(h5phtml);
this.markUpdated();
}
this.getDialogue({
focusAfterHide: null
}).hide();
},
/**
* Check if this could be a h5p URL.
*
* @method _updateWarning
* @param {String} str
* @return {boolean} whether a warning should be displayed.
* @private
*/
_validURL: function(str) {
var pattern = new RegExp('^(https?:\\/\\/)?' + // Protocol.
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // Domain name.
'((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address.
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'); // Port and path.
return !!pattern.test(str);
},
/**
* Update the url warning.
*
* @method _updateWarning
* @return {boolean} whether a warning should be displayed.
* @private
*/
_updateWarning: function() {
var form = this._form,
state = true,
url = form.one('.' + CSS.INPUTH5PURL).get('value');
if (this._validURL(url)) {
form.one('.' + CSS.URLWARNING).setStyle('display', 'none');
state = false;
} else {
form.one('.' + CSS.URLWARNING).setStyle('display', 'block');
state = true;
}
return state;
}
}, {
ATTRS: {
/**
* The allowedmethods of adding h5p content.
*
* @attribute allowedmethods
* @type String
*/
allowedmethods: {
value: null
}
}
});
}, '@VERSION@', {"requires": ["moodle-editor_atto-plugin"]});

View File

@ -0,0 +1 @@
YUI.add("moodle-atto_h5p-button",function(e,t){var n={INPUTALT:"atto_h5p_altentry",INPUTSUBMIT:"atto_h5p_urlentrysubmit",INPUTH5PURL:"atto_h5p_url",URLWARNING:"atto_h5p_warning"},r={INPUTH5PURL:"."+n.INPUTH5PURL},i="atto_h5p",s='<form class="atto_form"><div class="mb-4"><label for="{{elementid}}_{{CSS.INPUTH5PURL}}">{{get_string "enterurl" component}}</label><div style="display:none" role="alert" class="alert alert-warning mb-1 {{CSS.URLWARNING}}">{{get_string "invalidh5purl" component}}</div><input class="form-control fullwidth {{CSS.INPUTH5PURL}}" type="url" id="{{elementid}}_{{CSS.INPUTH5PURL}}" size="32"/></div><div class="text-center"><button class="btn btn-secondary {{CSS.INPUTSUBMIT}}" type="submit">{{get_string "saveh5p" component}}</button></div></form>',o='<div class="position-relative h5p-embed-placeholder"><div class="attoh5poverlay"></div><iframe id="h5pcontent" class="h5pcontent" src="{{url}}/embed" width="100%" height="637" frameborder="0"allowfullscreen="{{allowfullscreen}}" allowmedia="{{allowmedia}}"></iframe><script src="'+M.cfg.wwwroot+'/lib/editor/atto/plugins/h5p/js/h5p-resizer.js"'+'charset="UTF-8"></script>'+"</div>"+"</div>"+"<p><br></p>";e.namespace("M.atto_h5p").Button=e.Base.create("button",e.M.editor_atto.EditorPlugin,[],{_currentSelection:null,_form:null,_placeholderH5P:null,initializer:function(){var e=this.get("allowedmethods");if(e!=="embed")return;this.addButton({icon:"icon",iconComponent:"atto_h5p",callback:this._displayDialogue,tags:".attoh5poverlay",tagMatchRequiresAll:!1}),this.editor.delegate("dblclick",this._handleDblClick,".attoh5poverlay",this),this.editor.delegate("click",this._handleClick,".attoh5poverlay",this)},_handleDblClick:function(){this._displayDialogue()},_handleClick:function(e){var t=e.target,n=this.get("host").getSelectionFromNode(t);this.get("host").getSelection()!==n&&this.get("host").setSelection(n)},_displayDialogue:function(){this._currentSelection=this.get("host").getSelection(),this._placeholderH5P=this._getH5PIframe();if(this._currentSelection===!1)return;var e=this.getDialogue({headerContent:M.util.get_string("h5pproperties",i),width:"auto",focusAfterHide:!0,focusOnShowSelector:r.INPUTH5PURL});e.set("bodyContent",this._getDialogueContent()).show()},_getH5PIframe:function(){var t=this.get("host").getSelectionParentNode();if(!t)return;return e.one(t).one("iframe.h5pcontent")},_getDialogueContent:function(){var t=e.Handlebars.compile(s),o=e.Node.create(t({elementid:this.get("host").get("elementid"),CSS:n,component:i}));this._form=o;if(this._placeholderH5P){var u=this._placeholderH5P.getAttribute("src");this._form.one(r.INPUTH5PURL).setAttribute("value",u)}return this._form.one("."+n.INPUTSUBMIT).on("click",this._setH5P,this),o},_setH5P:function(t){var n=this._form,i=n.one(r.INPUTH5PURL).get("value"),s,u=this.get("host");t.preventDefault();if(this._updateWarning())return;u.focus();if(this._placeholderH5P)this._placeholderH5P.setAttribute("src",i);else if(i!==""){u.setSelection(this._currentSelection);var a=e.Handlebars.compile(o);s=a({url:i,allowfullscreen:"allowfullscreen",allowmedia:"geolocation *; microphone *; camera *; midi *; encrypted-media *"}),this.get("host").insertContentAtFocusPoint(s),this.markUpdated()}this.getDialogue({focusAfterHide:null}).hide()},_validURL:function(e){var t=new RegExp("^(https?:\\/\\/)?((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|((\\d{1,3}\\.){3}\\d{1,3}))(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*");return!!t.test(e)},_updateWarning:function(){var e=this._form,t=!0,r=e.one("."+n.INPUTH5PURL).get("value");return this._validURL(r)?(e.one("."+n.URLWARNING).setStyle("display","none"),t=!1):(e.one("."+n.URLWARNING).setStyle("display","block"),t=!0),t}},{ATTRS:{allowedmethods:{value:null}}})},"@VERSION@",{requires:["moodle-editor_atto-plugin"]});

View File

@ -0,0 +1,320 @@
YUI.add('moodle-atto_h5p-button', function (Y, NAME) {
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/*
* @package atto_h5p
* @copyright 2019 Bas Brands <bas@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* @module moodle-atto_h5p-button
*/
/**
* Atto h5p content tool.
*
* @namespace M.atto_h5p
* @class Button
* @extends M.editor_atto.EditorPlugin
*/
var CSS = {
INPUTALT: 'atto_h5p_altentry',
INPUTSUBMIT: 'atto_h5p_urlentrysubmit',
INPUTH5PURL: 'atto_h5p_url',
URLWARNING: 'atto_h5p_warning'
},
SELECTORS = {
INPUTH5PURL: '.' + CSS.INPUTH5PURL
},
COMPONENTNAME = 'atto_h5p',
TEMPLATE = '' +
'<form class="atto_form">' +
'<div class="mb-4">' +
'<label for="{{elementid}}_{{CSS.INPUTH5PURL}}">{{get_string "enterurl" component}}</label>' +
'<div style="display:none" role="alert" class="alert alert-warning mb-1 {{CSS.URLWARNING}}">' +
'{{get_string "invalidh5purl" component}}' +
'</div>' +
'<input class="form-control fullwidth {{CSS.INPUTH5PURL}}" type="url" ' +
'id="{{elementid}}_{{CSS.INPUTH5PURL}}" size="32"/>' +
'</div>' +
'<div class="text-center">' +
'<button class="btn btn-secondary {{CSS.INPUTSUBMIT}}" type="submit">' + '' +
'{{get_string "saveh5p" component}}</button>' +
'</div>' +
'</form>',
H5PTEMPLATE = '' +
'<div class="position-relative h5p-embed-placeholder">' +
'<div class="attoh5poverlay"></div>' +
'<iframe id="h5pcontent" class="h5pcontent" src="{{url}}/embed" ' +
'width="100%" height="637" frameborder="0"' +
'allowfullscreen="{{allowfullscreen}}" allowmedia="{{allowmedia}}">' +
'</iframe>' +
'<script src="' + M.cfg.wwwroot + '/lib/editor/atto/plugins/h5p/js/h5p-resizer.js"' +
'charset="UTF-8"></script>' +
'</div>' +
'</div>' +
'<p><br></p>';
Y.namespace('M.atto_h5p').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
/**
* A reference to the current selection at the time that the dialogue
* was opened.
*
* @property _currentSelection
* @type Range
* @private
*/
_currentSelection: null,
/**
* A reference to the currently open form.
*
* @param _form
* @type Node
* @private
*/
_form: null,
/**
* A reference to the currently selected H5P placeholder.
*
* @param _form
* @type Node
* @private
*/
_placeholderH5P: null,
initializer: function() {
var allowedmethods = this.get('allowedmethods');
if (allowedmethods !== 'embed') {
// Plugin not available here.
return;
}
this.addButton({
icon: 'icon',
iconComponent: 'atto_h5p',
callback: this._displayDialogue,
tags: '.attoh5poverlay',
tagMatchRequiresAll: false
});
this.editor.delegate('dblclick', this._handleDblClick, '.attoh5poverlay', this);
this.editor.delegate('click', this._handleClick, '.attoh5poverlay', this);
},
/**
* Handle a double click on a H5P Placeholder.
*
* @method _handleDblClick
* @private
*/
_handleDblClick: function() {
this._displayDialogue();
},
/**
* Handle a click on a H5P Placeholder.
*
* @method _handleClick
* @param {EventFacade} e
* @private
*/
_handleClick: function(e) {
var h5pplaceholder = e.target;
var selection = this.get('host').getSelectionFromNode(h5pplaceholder);
if (this.get('host').getSelection() !== selection) {
this.get('host').setSelection(selection);
}
},
/**
* Display the h5p editing tool.
*
* @method _displayDialogue
* @private
*/
_displayDialogue: function() {
// Store the current selection.
this._currentSelection = this.get('host').getSelection();
this._placeholderH5P = this._getH5PIframe();
if (this._currentSelection === false) {
return;
}
var dialogue = this.getDialogue({
headerContent: M.util.get_string('h5pproperties', COMPONENTNAME),
width: 'auto',
focusAfterHide: true,
focusOnShowSelector: SELECTORS.INPUTH5PURL
});
// Set the dialogue content, and then show the dialogue.
dialogue.set('bodyContent', this._getDialogueContent())
.show();
},
/**
* Get the H5P iframe
*
* @method _resolveH5P
* @return {Node} The H5P iframe selected.
* @private
*/
_getH5PIframe: function() {
var selectednode = this.get('host').getSelectionParentNode();
if (!selectednode) {
return;
}
return Y.one(selectednode).one('iframe.h5pcontent');
},
/**
* Return the dialogue content for the tool, attaching any required
* events.
*
* @method _getDialogueContent
* @return {Node} The content to place in the dialogue.
* @private
*/
_getDialogueContent: function() {
var template = Y.Handlebars.compile(TEMPLATE),
content = Y.Node.create(template({
elementid: this.get('host').get('elementid'),
CSS: CSS,
component: COMPONENTNAME
}));
this._form = content;
if (this._placeholderH5P) {
var oldurl = this._placeholderH5P.getAttribute('src');
this._form.one(SELECTORS.INPUTH5PURL).setAttribute('value', oldurl);
}
this._form.one('.' + CSS.INPUTSUBMIT).on('click', this._setH5P, this);
return content;
},
/**
* Set the h5p in the contenteditable.
*
* @method _setH5P
* @param {EventFacade} e
* @private
*/
_setH5P: function(e) {
var form = this._form,
url = form.one(SELECTORS.INPUTH5PURL).get('value'),
h5phtml,
host = this.get('host');
e.preventDefault();
// Check if there are any issues.
if (this._updateWarning()) {
return;
}
// Focus on the editor in preparation for inserting the h5p.
host.focus();
// If a H5P placeholder was selected we only update the placeholder.
if (this._placeholderH5P) {
this._placeholderH5P.setAttribute('src', url);
} else if (url !== '') {
host.setSelection(this._currentSelection);
var template = Y.Handlebars.compile(H5PTEMPLATE);
h5phtml = template({
url: url,
allowfullscreen: 'allowfullscreen',
allowmedia: 'geolocation *; microphone *; camera *; midi *; encrypted-media *'
});
this.get('host').insertContentAtFocusPoint(h5phtml);
this.markUpdated();
}
this.getDialogue({
focusAfterHide: null
}).hide();
},
/**
* Check if this could be a h5p URL.
*
* @method _updateWarning
* @param {String} str
* @return {boolean} whether a warning should be displayed.
* @private
*/
_validURL: function(str) {
var pattern = new RegExp('^(https?:\\/\\/)?' + // Protocol.
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // Domain name.
'((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address.
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'); // Port and path.
return !!pattern.test(str);
},
/**
* Update the url warning.
*
* @method _updateWarning
* @return {boolean} whether a warning should be displayed.
* @private
*/
_updateWarning: function() {
var form = this._form,
state = true,
url = form.one('.' + CSS.INPUTH5PURL).get('value');
if (this._validURL(url)) {
form.one('.' + CSS.URLWARNING).setStyle('display', 'none');
state = false;
} else {
form.one('.' + CSS.URLWARNING).setStyle('display', 'block');
state = true;
}
return state;
}
}, {
ATTRS: {
/**
* The allowedmethods of adding h5p content.
*
* @attribute allowedmethods
* @type String
*/
allowedmethods: {
value: null
}
}
});
}, '@VERSION@', {"requires": ["moodle-editor_atto-plugin"]});

View File

@ -0,0 +1,10 @@
{
"name": "moodle-atto_h5p-button",
"builds": {
"moodle-atto_h5p-button": {
"jsfiles": [
"button.js"
]
}
}
}

View File

@ -0,0 +1,315 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/*
* @package atto_h5p
* @copyright 2019 Bas Brands <bas@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
/**
* @module moodle-atto_h5p-button
*/
/**
* Atto h5p content tool.
*
* @namespace M.atto_h5p
* @class Button
* @extends M.editor_atto.EditorPlugin
*/
var CSS = {
INPUTALT: 'atto_h5p_altentry',
INPUTSUBMIT: 'atto_h5p_urlentrysubmit',
INPUTH5PURL: 'atto_h5p_url',
URLWARNING: 'atto_h5p_warning'
},
SELECTORS = {
INPUTH5PURL: '.' + CSS.INPUTH5PURL
},
COMPONENTNAME = 'atto_h5p',
TEMPLATE = '' +
'<form class="atto_form">' +
'<div class="mb-4">' +
'<label for="{{elementid}}_{{CSS.INPUTH5PURL}}">{{get_string "enterurl" component}}</label>' +
'<div style="display:none" role="alert" class="alert alert-warning mb-1 {{CSS.URLWARNING}}">' +
'{{get_string "invalidh5purl" component}}' +
'</div>' +
'<input class="form-control fullwidth {{CSS.INPUTH5PURL}}" type="url" ' +
'id="{{elementid}}_{{CSS.INPUTH5PURL}}" size="32"/>' +
'</div>' +
'<div class="text-center">' +
'<button class="btn btn-secondary {{CSS.INPUTSUBMIT}}" type="submit">' + '' +
'{{get_string "saveh5p" component}}</button>' +
'</div>' +
'</form>',
H5PTEMPLATE = '' +
'<div class="position-relative h5p-embed-placeholder">' +
'<div class="attoh5poverlay"></div>' +
'<iframe id="h5pcontent" class="h5pcontent" src="{{url}}/embed" ' +
'width="100%" height="637" frameborder="0"' +
'allowfullscreen="{{allowfullscreen}}" allowmedia="{{allowmedia}}">' +
'</iframe>' +
'<script src="' + M.cfg.wwwroot + '/lib/editor/atto/plugins/h5p/js/h5p-resizer.js"' +
'charset="UTF-8"></script>' +
'</div>' +
'</div>' +
'<p><br></p>';
Y.namespace('M.atto_h5p').Button = Y.Base.create('button', Y.M.editor_atto.EditorPlugin, [], {
/**
* A reference to the current selection at the time that the dialogue
* was opened.
*
* @property _currentSelection
* @type Range
* @private
*/
_currentSelection: null,
/**
* A reference to the currently open form.
*
* @param _form
* @type Node
* @private
*/
_form: null,
/**
* A reference to the currently selected H5P placeholder.
*
* @param _form
* @type Node
* @private
*/
_placeholderH5P: null,
initializer: function() {
var allowedmethods = this.get('allowedmethods');
if (allowedmethods !== 'embed') {
// Plugin not available here.
return;
}
this.addButton({
icon: 'icon',
iconComponent: 'atto_h5p',
callback: this._displayDialogue,
tags: '.attoh5poverlay',
tagMatchRequiresAll: false
});
this.editor.delegate('dblclick', this._handleDblClick, '.attoh5poverlay', this);
this.editor.delegate('click', this._handleClick, '.attoh5poverlay', this);
},
/**
* Handle a double click on a H5P Placeholder.
*
* @method _handleDblClick
* @private
*/
_handleDblClick: function() {
this._displayDialogue();
},
/**
* Handle a click on a H5P Placeholder.
*
* @method _handleClick
* @param {EventFacade} e
* @private
*/
_handleClick: function(e) {
var h5pplaceholder = e.target;
var selection = this.get('host').getSelectionFromNode(h5pplaceholder);
if (this.get('host').getSelection() !== selection) {
this.get('host').setSelection(selection);
}
},
/**
* Display the h5p editing tool.
*
* @method _displayDialogue
* @private
*/
_displayDialogue: function() {
// Store the current selection.
this._currentSelection = this.get('host').getSelection();
this._placeholderH5P = this._getH5PIframe();
if (this._currentSelection === false) {
return;
}
var dialogue = this.getDialogue({
headerContent: M.util.get_string('h5pproperties', COMPONENTNAME),
width: 'auto',
focusAfterHide: true,
focusOnShowSelector: SELECTORS.INPUTH5PURL
});
// Set the dialogue content, and then show the dialogue.
dialogue.set('bodyContent', this._getDialogueContent())
.show();
},
/**
* Get the H5P iframe
*
* @method _resolveH5P
* @return {Node} The H5P iframe selected.
* @private
*/
_getH5PIframe: function() {
var selectednode = this.get('host').getSelectionParentNode();
if (!selectednode) {
return;
}
return Y.one(selectednode).one('iframe.h5pcontent');
},
/**
* Return the dialogue content for the tool, attaching any required
* events.
*
* @method _getDialogueContent
* @return {Node} The content to place in the dialogue.
* @private
*/
_getDialogueContent: function() {
var template = Y.Handlebars.compile(TEMPLATE),
content = Y.Node.create(template({
elementid: this.get('host').get('elementid'),
CSS: CSS,
component: COMPONENTNAME
}));
this._form = content;
if (this._placeholderH5P) {
var oldurl = this._placeholderH5P.getAttribute('src');
this._form.one(SELECTORS.INPUTH5PURL).setAttribute('value', oldurl);
}
this._form.one('.' + CSS.INPUTSUBMIT).on('click', this._setH5P, this);
return content;
},
/**
* Set the h5p in the contenteditable.
*
* @method _setH5P
* @param {EventFacade} e
* @private
*/
_setH5P: function(e) {
var form = this._form,
url = form.one(SELECTORS.INPUTH5PURL).get('value'),
h5phtml,
host = this.get('host');
e.preventDefault();
// Check if there are any issues.
if (this._updateWarning()) {
return;
}
// Focus on the editor in preparation for inserting the h5p.
host.focus();
// If a H5P placeholder was selected we only update the placeholder.
if (this._placeholderH5P) {
this._placeholderH5P.setAttribute('src', url);
} else if (url !== '') {
host.setSelection(this._currentSelection);
var template = Y.Handlebars.compile(H5PTEMPLATE);
h5phtml = template({
url: url,
allowfullscreen: 'allowfullscreen',
allowmedia: 'geolocation *; microphone *; camera *; midi *; encrypted-media *'
});
this.get('host').insertContentAtFocusPoint(h5phtml);
this.markUpdated();
}
this.getDialogue({
focusAfterHide: null
}).hide();
},
/**
* Check if this could be a h5p URL.
*
* @method _updateWarning
* @param {String} str
* @return {boolean} whether a warning should be displayed.
* @private
*/
_validURL: function(str) {
var pattern = new RegExp('^(https?:\\/\\/)?' + // Protocol.
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // Domain name.
'((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address.
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'); // Port and path.
return !!pattern.test(str);
},
/**
* Update the url warning.
*
* @method _updateWarning
* @return {boolean} whether a warning should be displayed.
* @private
*/
_updateWarning: function() {
var form = this._form,
state = true,
url = form.one('.' + CSS.INPUTH5PURL).get('value');
if (this._validURL(url)) {
form.one('.' + CSS.URLWARNING).setStyle('display', 'none');
state = false;
} else {
form.one('.' + CSS.URLWARNING).setStyle('display', 'block');
state = true;
}
return state;
}
}, {
ATTRS: {
/**
* The allowedmethods of adding h5p content.
*
* @attribute allowedmethods
* @type String
*/
allowedmethods: {
value: null
}
}
});

View File

@ -0,0 +1,7 @@
{
"moodle-atto_h5p-button": {
"requires": [
"moodle-editor_atto-plugin"
]
}
}

View File

@ -36,7 +36,7 @@ if ($ADMIN->fulltree) {
style1 = title, bold, italic
list = unorderedlist, orderedlist
links = link
files = image, media, recordrtc, managefiles
files = image, media, recordrtc, managefiles, h5p
style2 = underline, strike, subscript, superscript
align = align
indent = indent

View File

@ -24,6 +24,6 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2019052000; // The current plugin version (Date: YYYYMMDDXX).
$plugin->version = 2019090900; // The current plugin version (Date: YYYYMMDDXX).
$plugin->requires = 2019051100; // Requires this Moodle version.
$plugin->component = 'editor_atto'; // Full name of the plugin (used for diagnostics).