From cc73ea075eb49d2e7a7824720d018d7240c5dbfb Mon Sep 17 00:00:00 2001 From: Adrian Greeve Date: Mon, 11 Jan 2016 15:54:43 +0800 Subject: [PATCH] MDL-52715 lib: Addition of new requirements class. This adds a fragment requirements class to allow plugins to register JavaScript to be used in an mform. --- lib/amd/build/fragment.min.js | 1 + lib/amd/src/fragment.js | 116 ++++++++++++++++ lib/db/services.php | 10 ++ lib/external/externallib.php | 75 ++++++++++ lib/outputfragmentrequirementslib.php | 191 ++++++++++++++++++++++++++ lib/outputlib.php | 1 + lib/pagelib.php | 7 + 7 files changed, 401 insertions(+) create mode 100644 lib/amd/build/fragment.min.js create mode 100644 lib/amd/src/fragment.js create mode 100644 lib/outputfragmentrequirementslib.php diff --git a/lib/amd/build/fragment.min.js b/lib/amd/build/fragment.min.js new file mode 100644 index 00000000000..15ce5ec2ff4 --- /dev/null +++ b/lib/amd/build/fragment.min.js @@ -0,0 +1 @@ +define(["jquery","core/ajax"],function(a,b){var c=function(c,d,e){var f=[];for(var g in e)f.push({name:g,value:e[g]});var h=a.Deferred(),i=b.call([{methodname:"core_get_fragment",args:{component:c,callback:d,args:f}}],!1);return i[0].done(function(a){h.resolve(a)}).fail(function(a){h.reject(a)}),h.promise()};return{fragment_load:function(a,b,d){return c(a,b,d)},fragment_append:function(b,d,e,f,g){a("#fragment-html").empty(),Y.on("#fragment-html").detach(),a("#fragment-html").remove(),a("#ajax-import-scripts").empty(),Y.on("#ajax-import-scripts").detach(),a("#ajax-import-scripts").remove(),a.when(c("mod_assign","fragment",e)).then(function(b){a(f).append('
'),a(f).append("
"),a("#fragment-html").append(b.html),a(g).append('
'),a(g).append("
"),a("#ajax-import-scripts").append(b.javascript)})}}}); \ No newline at end of file diff --git a/lib/amd/src/fragment.js b/lib/amd/src/fragment.js new file mode 100644 index 00000000000..bcefdcf2bdc --- /dev/null +++ b/lib/amd/src/fragment.js @@ -0,0 +1,116 @@ +// 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 . + +/** + * A way to call HTML fragments to be inserted as required via JavaScript. + * + * @module core/fragment + * @class fragment + * @package core + * @copyright 2016 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since 3.1 + */ +define(['jquery', 'core/ajax'], function($, ajax) { + + /** + * Loads an HTML fragment through a callback. + * + * @method load_fragment + * @param {string} component Component where callback is located. + * @param {string} callback Callback function name. + * @param {object} params Parameters for the callback. + * @return {Promise} JQuery promise object resolved when the fragment has been loaded. + */ + var load_fragment = function(component, callback, 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, + args: formattedparams + } + }], false); + + // Worth noting somewhere that the assign module seems to require userid, rownum etc. to be passed via POST / GET. + + promises[0].done(function(data) { + deferred.resolve(data); + }).fail(function(ex) { + deferred.reject(ex); + }); + return deferred.promise(); + }; + + return /** @alias module:core/fragment */{ + + /** + * Loads an HTML fragment through a callback. + * + * @method fragment_load + * @param {string} component Component where callback is located. + * @param {string} callback Callback function name. + * @param {object} params Parameters for the callback. + * @return {Promise} JQuery promise object resolved when the fragment has been loaded. + */ + fragment_load: function(component, callback, params) { + + return load_fragment(component, callback, params); + }, + + /** + * Appends HTML and JavaScript fragments to specified nodes. + * + * @method fragment_append + * @param {string} component Component where callback is located. + * @param {string} callback Callback function name. + * @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} + */ + fragment_append: function(component, callback, params, htmlnodeidentifier, javascriptnodeidentifier) { + // Clean up previous code if found first. + $('#fragment-html').empty(); + Y.on('#fragment-html').detach(); + $('#fragment-html').remove(); + $('#ajax-import-scripts').empty(); + Y.on('#ajax-import-scripts').detach(); + $('#ajax-import-scripts').remove(); + // $(".moodle-dialogue-base").empty(); + // $(".moodle-dialogue-base").remove(); + + $.when(load_fragment('mod_assign', "fragment", params)).then(function(data) { + // Attach new HTML and JavaScript. + $(htmlnodeidentifier).append("
"); + $(htmlnodeidentifier).append("
"); + $('#fragment-html').append(data.html); + + $(javascriptnodeidentifier).append("
"); + $(javascriptnodeidentifier).append("
"); + $('#ajax-import-scripts').append(data.javascript); + }); + } + }; +}); \ No newline at end of file diff --git a/lib/db/services.php b/lib/db/services.php index bc3230f3f4d..6707877aec8 100644 --- a/lib/db/services.php +++ b/lib/db/services.php @@ -1051,6 +1051,16 @@ $functions = array( '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', + 'loginrequired' => false, + 'ajax' => true, + ), + // === Calendar related functions === diff --git a/lib/external/externallib.php b/lib/external/externallib.php index a0526c53379..325f36027ac 100644 --- a/lib/external/externallib.php +++ b/lib/external/externallib.php @@ -260,4 +260,79 @@ class core_external extends external_api { '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_RAW, 'Component for the callback e.g. mod_asign'), + 'callback' => new external_value(PARAM_RAW, 'Name of the callback to execute'), + 'args' => new external_multiple_structure( + new external_single_structure( + array( + 'name' => new external_value(PARAM_ALPHANUMEXT, 'param name'), + 'value' => new external_value(PARAM_TEXT, '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. + * + * @param string $component Name of the component. + * @param string $callback Function callback name. + * @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, $args = null) { + global $PAGE; + + $params = self::validate_parameters(self::get_fragment_parameters(), + array( + 'component' => $component, + 'callback' => $callback, + 'args' => $args + ) + ); + + // Reformat arguments into something less unwieldy. + $arguments = array(); + foreach ($params['args'] as $paramargument) { + $arguments[$paramargument['name']] = $paramargument['value']; + } + + // Remove warning about context not being set. + $PAGE->set_context(context_system::instance()); + + $PAGE->set_requirements_for_fragments(); + $data = component_callback($params['component'], $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') + ) + ); + } } diff --git a/lib/outputfragmentrequirementslib.php b/lib/outputfragmentrequirementslib.php new file mode 100644 index 00000000000..235092be41b --- /dev/null +++ b/lib/outputfragmentrequirementslib.php @@ -0,0 +1,191 @@ +. + +/** + * Library functions to facilitate the use of JavaScript in Moodle. + * + * Note: you can find history of this file in lib/ajax/ajaxlib.php + * + * @copyright 2016 Adrian Greeve + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @package core + * @category output + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * This class tracks all the things that are needed by the current page. + * + * Normally, the only instance of this class you will need to work with is the + * one accessible via $PAGE->requires. + * + * Typical usage would be + *
+ *     $PAGE->requires->js_call_amd('mod_forum/view', 'init');
+ * 
+ * + * It also supports obsoleted coding style with/without YUI3 modules. + *
+ *     $PAGE->requires->js_init_call('M.mod_forum.init_view');
+ *     $PAGE->requires->css('/mod/mymod/userstyles.php?id='.$id); // not overridable via themes!
+ *     $PAGE->requires->js('/mod/mymod/script.js');
+ *     $PAGE->requires->js('/mod/mymod/small_but_urgent.js', true);
+ *     $PAGE->requires->js_function_call('init_mymod', array($data), true);
+ * 
+ * + * There are some natural restrictions on some methods. For example, {@link css()} + * can only be called before the tag is output. See the comments on the + * individual methods for details. + * + * @copyright 2016 Adrian Greeve + * @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 { + + /** + * Append YUI3 module to default YUI3 JS loader. + * The structure of module array is described at {@link http://developer.yahoo.com/yui/3/yui/} + * + * @param string|array $module name of module (details are autodetected), or full module specification as array + * @return void + */ + public function js_module($module) { + global $CFG; + + if (empty($module)) { + throw new coding_exception('Missing YUI3 module name or full description.'); + } + + if (is_string($module)) { + $module = $this->find_module($module); + } + + if (empty($module) or empty($module['name']) or empty($module['fullpath'])) { + throw new coding_exception('Missing YUI3 module details.'); + } + + $module['fullpath'] = $this->js_fix_url($module['fullpath'])->out(false); + // Add all needed strings. + if (!empty($module['strings'])) { + foreach ($module['strings'] as $string) { + $identifier = $string[0]; + $component = isset($string[1]) ? $string[1] : 'moodle'; + $a = isset($string[2]) ? $string[2] : null; + $this->string_for_js($identifier, $component, $a); + } + } + unset($module['strings']); + + // Process module requirements and attempt to load each. This allows + // moodle modules to require each other. + if (!empty($module['requires'])) { + foreach ($module['requires'] as $requirement) { + $rmodule = $this->find_module($requirement); + if (is_array($rmodule)) { + $this->js_module($rmodule); + } + } + } + + $this->extramodules[$module['name']] = $module; + } + + + /** + * 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. + * + * Normally, this method is called automatically by the code that prints the + * page footer. You should not normally need to call it in your own code. + * + * @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(); + + $this->js_init_code('M.util.js_complete("init");', true); + + // 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); + } + } + + // 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(js_writer::set_variable('M.str', $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 'bottom stuff'; + return $output; + + } +} diff --git a/lib/outputlib.php b/lib/outputlib.php index 64e02997da4..e438f718017 100644 --- a/lib/outputlib.php +++ b/lib/outputlib.php @@ -33,6 +33,7 @@ require_once($CFG->libdir.'/outputactions.php'); require_once($CFG->libdir.'/outputfactories.php'); require_once($CFG->libdir.'/outputrenderers.php'); require_once($CFG->libdir.'/outputrequirementslib.php'); +require_once($CFG->libdir.'/outputfragmentrequirementslib.php'); /** * Invalidate all server and client side caches. diff --git a/lib/pagelib.php b/lib/pagelib.php index d46b77d8f54..3fcf08e6875 100644 --- a/lib/pagelib.php +++ b/lib/pagelib.php @@ -824,6 +824,13 @@ class moodle_page { return $this->_navbar->has_items(); } + /** + * Changes the requirements manager over to receive fragments. + */ + public function set_requirements_for_fragments() { + $this->_requires = new fragment_requirements_manager(); + } + /** * Should the current user see this page in editing mode. * That is, are they allowed to edit this page, and are they currently in