MDL-52715 lib: Addition of new requirements class.
authorAdrian Greeve <adrian@moodle.com>
Mon, 11 Jan 2016 07:54:43 +0000 (15:54 +0800)
committerAdrian Greeve <adrian@moodle.com>
Mon, 1 Feb 2016 06:29:09 +0000 (14:29 +0800)
This adds a fragment requirements class to allow plugins
to register JavaScript to be used in an mform.

lib/amd/build/fragment.min.js [new file with mode: 0644]
lib/amd/src/fragment.js [new file with mode: 0644]
lib/db/services.php
lib/external/externallib.php
lib/outputfragmentrequirementslib.php [new file with mode: 0644]
lib/outputlib.php
lib/pagelib.php

diff --git a/lib/amd/build/fragment.min.js b/lib/amd/build/fragment.min.js
new file mode 100644 (file)
index 0000000..15ce5ec
Binary files /dev/null and b/lib/amd/build/fragment.min.js differ
diff --git a/lib/amd/src/fragment.js b/lib/amd/src/fragment.js
new file mode 100644 (file)
index 0000000..bcefdcf
--- /dev/null
@@ -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 <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'], 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("<div id=\"fragment-html\">");
+                $(htmlnodeidentifier).append("</div>");
+                $('#fragment-html').append(data.html);
+
+                $(javascriptnodeidentifier).append("<div id=\"ajax-import-scripts\">");
+                $(javascriptnodeidentifier).append("</div>");
+                $('#ajax-import-scripts').append(data.javascript);
+            });
+        }
+    };
+});
\ No newline at end of file
index bc3230f..6707877 100644 (file)
@@ -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 ===
 
index a0526c5..325f360 100644 (file)
@@ -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 (file)
index 0000000..235092b
--- /dev/null
@@ -0,0 +1,191 @@
+<?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.
+ *
+ * Note: you can find history of this file in lib/ajax/ajaxlib.php
+ *
+ * @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 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
+ * <pre>
+ *     $PAGE->requires->js_call_amd('mod_forum/view', 'init');
+ * </pre>
+ *
+ * It also supports obsoleted coding style with/without YUI3 modules.
+ * <pre>
+ *     $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);
+ * </pre>
+ *
+ * There are some natural restrictions on some methods. For example, {@link css()}
+ * can only be called before the <head> tag is output. See the comments on the
+ * individual methods for details.
+ *
+ * @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 {
+
+    /**
+     * 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;
+
+    }
+}
index 64e0299..e438f71 100644 (file)
@@ -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.
index d46b77d..3fcf08e 100644 (file)
@@ -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