Skip to content

Commit

Permalink
Merge branch 'wip-MDL-52715-master-additional' of git://github.com/ab…
Browse files Browse the repository at this point in the history
…greeve/moodle
  • Loading branch information
danpoltawski committed Feb 3, 2016
2 parents 6d18da5 + a2161d5 commit 96c6942
Show file tree
Hide file tree
Showing 7 changed files with 365 additions and 1 deletion.
1 change: 1 addition & 0 deletions lib/amd/build/fragment.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

118 changes: 118 additions & 0 deletions lib/amd/src/fragment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// 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/>.

/**
* A way to call HTML fragments to be inserted as required via JavaScript.
*
* @module core/fragment
* @class fragment
* @package core
* @copyright 2016 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since 3.1
*/
define(['jquery', 'core/ajax', 'core/notification'], function($, ajax, notification) {

/**
* Loads an HTML fragment through a callback.
*
* @method loadFragment
* @param {string} component Component where callback is located.
* @param {string} callback Callback function name.
* @param {integer} contextid Context ID of the fragment.
* @param {object} params Parameters for the callback.
* @return {Promise} JQuery promise object resolved when the fragment has been loaded.
*/
var loadFragment = function(component, callback, contextid, params) {
// Change params into required webservice format.
var formattedparams = [];
for (var index in params) {
formattedparams.push({name: index, value: params[index]});
}

// Ajax stuff.
var deferred = $.Deferred();

var promises = ajax.call([{
methodname: 'core_get_fragment',
args:{
component: component,
callback: callback,
contextid: contextid,
args: formattedparams
}
}], false);

promises[0].done(function(data) {
deferred.resolve(data);
}).fail(function(ex) {
deferred.reject(ex);
});
return deferred.promise();
};

/**
* Removes and cleans children of a node. This includes event handlers and listeners that may be
* attached to the nodes for both jquery and yui.
*
* @method recursiveCleanup
* @param {object} DOM node to be cleaned.
* @return {void}
*/
var recursiveCleanup = function(node) {
node.children().each(function(index, el) {
var child = $(el);
recursiveCleanup(child);
});
var yuinode = new Y.Node(node);
node.empty();
node.remove();
yuinode.detachAll();
if (yuinode.get('childNodes')) {
yuinode.empty();
}
yuinode.remove(true);
};

return /** @alias module:core/fragment */{
/**
* Appends HTML and JavaScript fragments to specified nodes.
* Callbacks called by this AMD module are responsible for doing the appropriate security checks
* to access the information that is returned. This only does minimal validation on the context.
*
* @method fragmentAppend
* @param {string} component Component where callback is located.
* @param {string} callback Callback function name.
* @param {integer} contextid Context ID of the fragment.
* @param {object} params Parameters for the callback.
* @param {string} htmlnodeidentifier The 'class' or 'id' to attach the HTML.
* @param {string} javascriptnodeidentifier The 'class' or 'id' to attach the JavaScript.
* @return {void}
*/
fragmentAppend: function(component, callback, contextid, params, htmlnodeidentifier, javascriptnodeidentifier) {
$.when(loadFragment(component, callback, contextid, params)).then(function(data) {
// Clean up previous code if found first.
recursiveCleanup($('#fragment-html'));
recursiveCleanup($('#fragment-scripts'));
// Attach new HTML and JavaScript.
$(htmlnodeidentifier).append('<div id="fragment-html">' + data.html + '</div>');
$(javascriptnodeidentifier).append('<div id="fragment-scripts">' + data.javascript + '</div>');

}).fail(function(ex) {
notification.exception(ex);
});
}
};
});
9 changes: 9 additions & 0 deletions lib/db/services.php
Original file line number Diff line number Diff line change
Expand Up @@ -1051,6 +1051,15 @@
'ajax' => true,
),

'core_get_fragment' => array(
'classname' => 'core_external',
'methodname' => 'get_fragment',
'classpath' => 'lib/external/externallib.php',
'description' => 'Return a fragment for inclusion, such as a JavaScript page.',
'type' => 'read',
'ajax' => true,
),


// === Calendar related functions ===

Expand Down
86 changes: 86 additions & 0 deletions lib/external/externallib.php
Original file line number Diff line number Diff line change
Expand Up @@ -260,4 +260,90 @@ public static function get_component_strings_returns() {
'string' => new external_value(PARAM_RAW, 'translated string'))
));
}

/**
* Returns description of get_fragment parameters
*
* @return external_function_parameters
* @since Moodle 3.1
*/
public static function get_fragment_parameters() {
return new external_function_parameters(
array(
'component' => new external_value(PARAM_COMPONENT, 'Component for the callback e.g. mod_assign'),
'callback' => new external_value(PARAM_ALPHANUMEXT, 'Name of the callback to execute'),
'contextid' => new external_value(PARAM_INT, 'Context ID that the fragment is from'),
'args' => new external_multiple_structure(
new external_single_structure(
array(
'name' => new external_value(PARAM_ALPHANUMEXT, 'param name'),
'value' => new external_value(PARAM_RAW, 'param value')
)
), 'args for the callback are optional', VALUE_OPTIONAL
)
)
);
}

/**
* Get a HTML fragment for inserting into something. Initial use is for inserting mforms into
* a page using AJAX.
* This web service is designed to be called only via AJAX and not directly.
* Callbacks that are called by this web service are responsible for doing the appropriate security checks
* to access the information returned. This only does minimal validation on the context.
*
* @param string $component Name of the component.
* @param string $callback Function callback name.
* @param int $contextid Context ID this fragment is in.
* @param array $args optional arguments for the callback.
* @return array HTML and JavaScript fragments for insertion into stuff.
* @since Moodle 3.1
*/
public static function get_fragment($component, $callback, $contextid, $args = null) {
global $OUTPUT, $PAGE;

$params = self::validate_parameters(self::get_fragment_parameters(),
array(
'component' => $component,
'callback' => $callback,
'contextid' => $contextid,
'args' => $args
)
);

// Reformat arguments into something less unwieldy.
$arguments = array();
foreach ($params['args'] as $paramargument) {
$arguments[$paramargument['name']] = $paramargument['value'];
}

$context = context::instance_by_id($contextid);
self::validate_context($context);

// Hack alert: Forcing bootstrap_renderer to initiate moodle page.
$OUTPUT->header();

// Overwriting page_requirements_manager with the fragment one so only JS included from
// this point is returned to the user.
$PAGE->start_collecting_javascript_requirements();
$data = component_callback($params['component'], 'output_fragment_' . $params['callback'], $arguments);
$jsfooter = $PAGE->requires->get_end_code();
$output = array('html' => $data, 'javascript' => $jsfooter);
return $output;
}

/**
* Returns description of get_fragment() result value
*
* @return array
* @since Moodle 3.1
*/
public static function get_fragment_returns() {
return new external_single_structure(
array(
'html' => new external_value(PARAM_RAW, 'HTML fragment.'),
'javascript' => new external_value(PARAM_RAW, 'JavaScript fragment')
)
);
}
}
127 changes: 127 additions & 0 deletions lib/outputfragmentrequirementslib.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?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/>.

/**
* Library functions to facilitate the use of JavaScript in Moodle.
*
* @copyright 2016 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @package core
* @category output
*/

defined('MOODLE_INTERNAL') || die();

/**
* This requirements manager captures the appropriate html for creating a fragment to
* be inserted elsewhere.
*
* @copyright 2016 Adrian Greeve <adrian@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 3.1
* @package core
* @category output
*/
class fragment_requirements_manager extends page_requirements_manager {

/**
* Page fragment constructor.
*/
public function __construct() {
parent::__construct();
// As this is a fragment the header should already be done.
$this->headdone = true;
}

/**
* Returns js code to load amd module loader, then insert inline script tags
* that contain require() calls using RequireJS.
*
* @return string
*/
protected function get_amd_footercode() {
global $CFG;
$output = '';

// First include must be to a module with no dependencies, this prevents multiple requests.
$prefix = "require(['core/first'], function() {\n";
$suffix = "\n});";
$output .= html_writer::script($prefix . implode(";\n", $this->amdjscode) . $suffix);
return $output;
}


/**
* Generate any HTML that needs to go at the end of the page.
*
* @return string the HTML code to to at the end of the page.
*/
public function get_end_code() {
global $CFG;

$output = '';

// Call amd init functions.
$output .= $this->get_amd_footercode();

// Add other requested modules.
$output .= $this->get_extra_modules_code();

// All the other linked scripts - there should be as few as possible.
if ($this->jsincludes['footer']) {
foreach ($this->jsincludes['footer'] as $url) {
$output .= html_writer::script('', $url);
}
}

if (!empty($this->stringsforjs)) {
// Add all needed strings.
$strings = array();
foreach ($this->stringsforjs as $component => $v) {
foreach ($v as $indentifier => $langstring) {
$strings[$component][$indentifier] = $langstring->out();
}
}
// Append don't overwrite.
$output .= html_writer::script('require(["jquery"], function($) {
M.str = $.extend(true, M.str, ' . json_encode($strings) . ');
});');
}

// Add variables.
if ($this->jsinitvariables['footer']) {
$js = '';
foreach ($this->jsinitvariables['footer'] as $data) {
list($var, $value) = $data;
$js .= js_writer::set_variable($var, $value, true);
}
$output .= html_writer::script($js);
}

$inyuijs = $this->get_javascript_code(false);
$ondomreadyjs = $this->get_javascript_code(true);
// See if this is still needed when we get to the ajax page.
$jsinit = $this->get_javascript_init_code();
$handlersjs = $this->get_event_handler_code();

// There is a global Y, make sure it is available in your scope.
$js = "(function() {{$inyuijs}{$ondomreadyjs}{$jsinit}{$handlersjs}})();";

$output .= html_writer::script($js);

return $output;
}
}
23 changes: 23 additions & 0 deletions lib/pagelib.php
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,29 @@ public function has_navbar() {
return $this->_navbar->has_items();
}

/**
* Switches from the regular requirements manager to the fragment requirements manager to
* capture all necessary JavaScript to display a chunk of HTML such as an mform. This is for use
* by the get_fragment() web service and not for use elsewhere.
*/
public function start_collecting_javascript_requirements() {
global $CFG;
require_once($CFG->libdir.'/outputfragmentrequirementslib.php');

// Check that the requirements manager has not already been switched.
if (get_class($this->_requires) == 'fragment_requirements_manager') {
throw new coding_exception('JavaScript collection has already been started.');
}
// The header needs to have been called to flush out the generic JavaScript for the page. This allows only
// JavaScript for the fragment to be collected. _wherethemewasinitialised is set when header() is called.
if (!empty($this->_wherethemewasinitialised)) {
// Change the current requirements manager over to the fragment manager to capture JS.
$this->_requires = new fragment_requirements_manager();
} else {
throw new coding_exception('$OUTPUT->header() needs to be called before collecting JavaScript requirements.');
}
}

/**
* Should the current user see this page in editing mode.
* That is, are they allowed to edit this page, and are they currently in
Expand Down
2 changes: 1 addition & 1 deletion version.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

defined('MOODLE_INTERNAL') || die();

$version = 2016020201.00; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2016020300.00; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.

Expand Down

0 comments on commit 96c6942

Please sign in to comment.