]> git.mjollnir.org Git - moodle.git/commitdiff
ajaxlib: MDL-16695 New page_requirements_manager class. Soon to replace require_js.
authortjhunt <tjhunt>
Fri, 12 Jun 2009 03:13:29 +0000 (03:13 +0000)
committertjhunt <tjhunt>
Fri, 12 Jun 2009 03:13:29 +0000 (03:13 +0000)
This new class does the work that require_js used to do, and more.
It can track a number of different things that may be required to
be output somewhere on the page, including:

    * Links to JS files
    * Links to CSS files
    * Links to YUI libraries (this class knows about the dependancies between the different libs).
    * Skip links that go from the top of <body> to various places in the content.
    * Calls to JavaScript functions (for example to initialise things)
    * Bits of data from PHP that need to be available to JavaScript
    * As a special case of that, an easy way to pass language strings to JS.

The new API looks like

$PAGE->requires->css('mod/mymod/styles.css');
$PAGE->requires->js('mod/mymod/script.js');
$PAGE->requires->js('mod/mymod/small_but_urgent.js')->in_head();
$PAGE->requires->js_function_call('init_mymod', array($data))->on_dom_ready();

$PAGE->requires is the canonical instances of this new class.

The commit also includes unit tests, and hopefully the PHP doc comments are
clear enough that it is easy to understand.

lang/en_utf8/access.php
lib/ajax/ajaxlib.php
lib/ajax/simpletest/testajaxlib.php
lib/pagelib.php

index 0331636991c24e1f624cd8d4d59873b35072e7b0..02fb0ac7b1f3ef5dfc66c495a8f8fd2508d3ccb4 100644 (file)
@@ -21,6 +21,7 @@ $string['sitemap'] = 'Site map';
 $string['skipa'] = 'Skip $a';
 $string['skipblock'] = 'Skip block';
 $string['skipnavigation'] = 'Skip navigation';
+$string['skipto'] = 'Skip to $a';
 $string['tabledata'] = 'Data table, $a';
 $string['tablelayout'] = 'Layout table, $a';
 $string['tocontent'] = 'Skip to main content';
index 06b526b94762ad1923be1ba06ed75a0fd1983fda..e0aeaa2c891cba6dfc3e0e99dabcd6371af7c27f 100644 (file)
 <?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.
+ *
+ * @package   moodlecore
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+/**
+ * Initialise a page_requirements_manager with the bits of JavaScript that every
+ * Moodle page needs.
+ *
+ * @param page_requirements_manager $requires The page_requirements_manager to initialise.
+ */
+function setup_core_javascript(page_requirements_manager $requires) {
+    global $CFG;
+
+    // JavaScript should always work with $CFG->httpswwwroot rather than $CFG->wwwroot.
+    // Otherwise, in some situations, users will get warnings about insecure content
+    // on sercure pages from their web browser.
+
+    $config = array(
+        'wwwroot' => $CFG->httpswwwroot, // Yes, really. See above.
+        'pixpath' => $CFG->pixpath,
+        'modpixpath' => $CFG->modpixpath,
+        'sesskey' => sesskey(),
+    );
+    if (debugging('', DEBUG_DEVELOPER)) {
+        $config['developerdebug'] = true;
+    }
+    $requires->data_for_js('moodle_cfg', $config)->in_head();
+
+    // Note that, as a short-cut, the code 
+    // $js = "document.body.className += ' jsenabled';\n";
+    // is hard-coded in @see{page_requirements_manager::get_top_of_body_code}.
+}
+
+
 /**
- * Library functions for using AJAX with Moodle.
+ * 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 useage would be
+ *     $PAGE->requires->css('mod/mymod/styles.css');
+ *     $PAGE->requires->js('mod/mymod/script.js');
+ *     $PAGE->requires->js('mod/mymod/small_but_urgent.js')->in_head();
+ *     $PAGE->requires->js_function_call('init_mymod', array($data))->on_dom_ready();
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
+class page_requirements_manager {
+    const WHEN_IN_HEAD = 0;
+    const WHEN_TOP_OF_BODY = 10;
+    const WHEN_AT_END = 20;
+    const WHEN_ON_DOM_READY = 30;
+
+    protected $linkedrequiremets = array();
+    protected $stringsforjs = array();
+    protected $requiredjscode = array();
+
+    protected $variablesinitialised = array('mstr' => 1); // 'mstr' is special. See string_for_js.
+
+    protected $headdone = false;
+    protected $topofbodydone = false;
+
+    /**
+     * Ensure that the specified script file is linked to from this page. By default
+     * the link is put at the end of the page, since this gives best load performance.
+     *
+     * Even if a particular script is requested more than once, it will only be linked
+     * to once.
+     *
+     * @param $jsfile The path to the .js file, relative to $CFG->dirroot / $CFG->wwwroot.
+     *      No leading slash. For example 'mod/mymod/customscripts.js';
+     * @param boolean $fullurl This parameter is intended for internal use only.
+     *      (If true, $jsfile is treaded as a full URL, not relative $CFG->wwwroot.)
+     * @return required_js A required_js object. This allows you to control when the
+     *      link to the script is output by calling methods like ->asap() or
+     *      ->in_head().
+     */
+    public function js($jsfile, $fullurl = false) {
+        global $CFG;
+        if (!$fullurl) {
+            if (!file_exists($CFG->dirroot . '/' . $jsfile)) {
+                throw new coding_exception('Attept to require a JavaScript file that does not exist.', $jsfile);
+            }
+            $url = $CFG->httpswwwroot . '/' . $jsfile;
+        } else {
+            $url = $jsfile;
+        }
+        if (!isset($this->linkedrequiremets[$url])) {
+            $this->linkedrequiremets[$url] = new required_js($this, $url);
+        }
+        return $this->linkedrequiremets[$url];
+    }
+
+    /**
+     * Ensure that the specified YUI library file, and all its required dependancies,
+     * are linked to from this page. By default the link is put at the end of the
+     * page, since this gives best load performance. Optional dependencies are not
+     * loaded automatically - if you want them you will need to load them first with
+     * other calls to this method.
+     *
+     * If the YUI library you ask for requires one or more CSS files, and if
+     * &lt;head> has already been printed, then an exception will be thrown.
+     *
+     * Even if a particular library is requested more than once (perhaps as a dependancy
+     * of other libraries) it will only be linked to once.
+     *
+     * @param $libname the name of the YUI library you require. For example 'autocomplete'.
+     * @return required_yui_lib A requried_yui_lib object. This allows you to control when the
+     *      link to the script is output by calling methods like ->asap() or
+     *      ->in_head().
+     */
+    public function yui_lib($libname) {
+        $key = 'yui:' . $libname;
+        if (!isset($this->linkedrequiremets[$key])) {
+            $this->linkedrequiremets[$key] = new required_yui_lib($this, $libname);
+        }
+        return $this->linkedrequiremets[$key];
+    }
+
+    /**
+     * Ensure that the specified CSS file is linked to from this page. Because
+     * stylesheet links must go in the &lt;head> part of the HTML, you must call
+     * this function before get_head_code is called. That normally means before
+     * the call to print_header. If you call it when it is too late, an exception
+     * will be thrown.
+     *
+     * Even if a particular style sheet is requested more than once, it will only
+     * be linked to once.
+     *
+     * @param string $stylesheet The path to the .js file, relative to
+     *      $CFG->dirroot / $CFG->wwwroot. No leading slash. For example
+     *      'mod/mymod/styles.css';
+     * @param boolean $fullurl This parameter is intended for internal use only.
+     *      (If true, $stylesheet is treaded as a full URL, not relative $CFG->wwwroot.)
+     */
+    public function css($stylesheet, $fullurl = false) {
+        global $CFG;
+
+        if ($this->headdone) {
+            throw new coding_exception('Cannot require a CSS file after <head> has been printed.', $stylesheet);
+        }
+        if (!$fullurl) {
+            if (!file_exists($CFG->dirroot . '/' . $stylesheet)) {
+                throw new coding_exception('Attept to require a CSS file that does not exist.', $stylesheet);
+            }
+            $url = $CFG->httpswwwroot . '/' . $stylesheet;
+        } else {
+            $url = $stylesheet;
+        }
+        if (!isset($this->linkedrequiremets[$url])) {
+            $this->linkedrequiremets[$url] = new required_css($this, $url);
+        }
+    }
+
+    /**
+     * Ensure that a skip link to a given target is printed at the top of the &lt;body>.
+     * You must call this function before get_top_of_body_code, (if not, an exception
+     * will be thrown). That normally means you must call this before the call to print_header.
+     *
+     * If you ask for a particular skip link to be printed, it is then your responsibility
+     * to ensure that the appropraite &lt;a name="..."> tag is printed in the body of the
+     * page, so that the skip link goes somewhere.
+     *
+     * Even if a particular skip link is requested more than once, only one copy of it will be output.
+     *
+     * @param $target the name of anchor this link should go to. For example 'maincontent'.
+     * @param $linktext The text to use for the skip link. Normally get_string('skipto', 'access', ...);
+     */
+    public function skip_link_to($target, $linktext) {
+        if (!isset($this->linkedrequiremets[$target])) {
+            $this->linkedrequiremets[$target] = new required_skip_link($this, $target, $linktext);
+        }
+    }
+
+    /**
+     * Ensure that the specified JavaScript function is called from an inline script
+     * somewhere on this page. By default the call will be put in a script tag at the
+     * end of the page, since this gives best page-load performance.
+     *
+     * If you request that a particular function is called several times, then
+     * that is what will happen (unlike linking to a CSS or JS file, where only
+     * one link will be output).
+     *
+     * @param string $function the name of the JavaScritp function to call. Can
+     *      be a compound name like 'YAHOO.util.Event.addListener'.
+     * @param array $arguments and array of arguments to be passed to the function.
+     *      When generating the function call, this will be escaped using json_encode,
+     *      so passing objects and arrays should work.
+     * @return required_js_function_call A required_js_function_call object.
+     *      This allows you to control when the link to the script is output by
+     *      calling methods like ->asap(), ->in_head(), ->at_top_of_body() or
+     *      ->on_dom_ready() methods.
+     */
+    public function js_function_call($function, $arguments = array()) {
+        $requirement = new required_js_function_call($this, $function, $arguments);
+        $this->requiredjscode[] = $requirement;
+        return $requirement;
+    }
+
+    /**
+     * Make a language string available to JavaScript. All the strings will be
+     * available in a mstr object in the global namespace. So, for example,
+     * after a call to $PAGE->requires->string_for_js('course', 'moodle');
+     * then the JavaScript variable mstr.moodle.course will be 'Course', or the
+     * equivalent in the current language.
+     *
+     * The arguments to this function are just like the arguments to get_string
+     * except that $module is not optional, and there are limitations on how you
+     * use $a. Because each string is only stored once in the JavaScript (based
+     * on $identifier and $module) you cannot get the same string with two different
+     * values of $a. If you try, an exception will be thrown.
+     *
+     * If you do need the same string expanded with different $a values, then
+     * the solution is to put them in your own data structure (e.g. and array)
+     * that you pass to JavaScript with @see{data_for_js}.
+     *
+     * @param string $identifier the desired string.
+     * @param string $module the language file to look in.
+     * @param mixed $a any extra data to add into the string (optional).
+     */
+    public function string_for_js($identifier, $module, $a = NULL) {
+        $string = get_string($identifier, $module, $a);
+        if (!$module) {
+            $module = 'moodle';
+        }
+        if (isset($this->stringsforjs[$module][$identifier]) && $this->stringsforjs[$module][$identifier] != $string) {
+            throw new coding_exception("Attempt to re-define already required string '$identifier' " .
+                    "from lang file '$module'. Did you already ask for it with a different \$a?");
+        }
+        $this->stringsforjs[$module][$identifier] = $string;
+    }
+
+    /**
+     * Make some data from PHP available to JavaScript code. For example, if you call
+     *      $PAGE->requires->data_for_js('mydata', array('name' => 'Moodle'));
+     * then in JavsScript mydata.name will be 'Moodle'.
+     *
+     * You cannot call this function more than once with the same variable name
+     * (if you try, it will throw an exception). Your code should prepare all the
+     * date you want, and then pass it to this method. There is no way to change
+     * the value associated with a particular variable later.
+     *
+     * @param string $variable the the name of the JavaScript variable to assign the data to.
+     *      Will probably work if you use a compound name like 'mybuttons.button[1]', but this
+     *      should be considered an experimental feature.
+     * @param mixed $data The data to pass to JavaScript. This will be escaped using json_encode,
+     *      so passing objects and arrays should work.
+     * @return required_data_for_js A required_data_for_js object.
+     *      This allows you to control when the link to the script is output by
+     *      calling methods like ->asap(), ->in_head(), ->at_top_of_body() or
+     *      ->on_dom_ready() methods.
+     */
+    public function data_for_js($variable, $data) {
+        if (isset($this->variablesinitialised[$variable])) {
+            throw new coding_exception("A variable called '" . $variable .
+                    "' has already been passed ot JavaScript. You cannot overwrite it.");
+        }
+        $requirement = new required_data_for_js($this, $variable, $data);
+        $this->requiredjscode[] = $requirement;
+        $this->variablesinitialised[$variable] = 1;
+        return $requirement;
+    }
+
+    /**
+     * Get the code for the linked resources that need to appear in a particular place.
+     * @param $when one of the WHEN_... constants.
+     * @return string the HTML that should be output in that place.
+     */
+    protected function get_linked_resources_code($when) {
+        $output = '';
+        foreach ($this->linkedrequiremets as $requirement) {
+            if (!$requirement->is_done() && $requirement->get_when() == $when) {
+                $output .= $requirement->get_html();
+                $requirement->mark_done();
+            }
+        }
+        return $output;
+    }
+
+    /**
+     * Get the code for the linked resources that need to appear in a particular place.
+     * @param $when one of the WHEN_... constants.
+     * @return string the javascript that should be output in that place.
+     */
+    protected function get_javascript_code($when, $indent = '') {
+        $output = '';
+        foreach ($this->requiredjscode as $requirement) {
+            if (!$requirement->is_done() && $requirement->get_when() == $when) {
+                $output .= $indent . $requirement->get_js_code();
+                $requirement->mark_done();
+            }
+        }
+        return $output;
+    }
+
+    /**
+     * Generate any HTML that needs to go inside the &lt;head> tag.
+     *
+     * @return string the HTML code to to inside the &lt;head> tag.
+     */
+    public function get_head_code() {
+        $output = $this->get_linked_resources_code(self::WHEN_IN_HEAD);
+        $js = $this->get_javascript_code(self::WHEN_IN_HEAD);
+        $output .= ajax_generate_script_tag($js);
+        $this->headdone = true;
+        return $output;
+    }
+
+    /**
+     * Generate any HTML that needs to go at the start of the &lt;body> tag.
+     *
+     * @return string the HTML code to go at the start of the &lt;body> tag.
+     */
+    public function get_top_of_body_code() {
+        $output = $this->get_linked_resources_code(self::WHEN_TOP_OF_BODY);
+        $js = "document.body.className += ' jsenabled';\n";
+        $js .= $this->get_javascript_code(self::WHEN_TOP_OF_BODY);
+        $output .= ajax_generate_script_tag($js);
+        $this->topofbodydone = true;
+        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() {
+        $output = $this->get_linked_resources_code(self::WHEN_AT_END);
+
+        array_unshift($this->requiredjscode, new required_data_for_js($this, 'mstr', $this->stringsforjs));
+        $js = $this->get_javascript_code(self::WHEN_AT_END);
+
+        $ondomreadyjs = $this->get_javascript_code(self::WHEN_ON_DOM_READY, '    ');
+        if ($ondomreadyjs) {
+            $js .= "YAHOO.util.Event.onDOMReady(function() {\n" . $ondomreadyjs . "});\n";
+        }
+
+        $output .= ajax_generate_script_tag($js);
+
+        return $output;
+    }
+
+    /**
+     * Have we already output the code in the <lt;head> tag?
+     *
+     * @return boolean
+     */
+    public function is_head_done() {
+        return $this->headdone;
+    }
+
+    /**
+     * Have we already output the code at the start of the <lt;body> tag?
+     *
+     * @return boolean
+     */
+    public function is_top_of_body_done() {
+        return $this->topofbodydone;
+    }
+}
+
+
+/**
+ * This is the base class for all sorts of requirements. just to factor out some
+ * common code.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class requirement_base {
+    protected $manager;
+    protected $when;
+    protected $done = false;
+
+    /**
+     * Constructor. Normally the class and its subclasses should not be created
+     * directly. Client code should create them via a page_requirements_manager
+     * method like ->js(...).
+     *
+     * @param page_requirements_manager $manager the page_requirements_manager we are associated with.
+     */
+    protected function __construct(page_requirements_manager $manager) {
+        $this->manager = $manager;
+    }
+
+    /**
+     * Mark that this requirement has been satisfied (that is, that the HTML
+     * returned by @see{get_html} has been output.
+     * @return boolean has this requirement been satisfied yet? That is, has
+     *      that the HTML returned by @see{get_html} has been output already.
+     */
+    public function is_done() {
+        return $this->done;
+    }
+
+    /**
+     * Mark that this requirement has been satisfied (that is, that the HTML
+     * returned by @see{get_html} has been output.
+     */
+    public function mark_done() {
+        $this->done = true;
+    }
+
+    /**
+     * Where on the page the HTML this requirement is meant to go.
+     * @return integer One of the page_requirements_manager::WHEN_... constants.
+     */
+    public function get_when() {
+        return $this->when;
+    }
+}
+
+/**
+ * This class represents something that must be output somewhere in the HTML.
+ *
+ * Examples include links to JavaScript or CSS files. However, it should not
+ * necessarily be output immediately, we may have to wait for an appropriate time.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class linked_requirement extends requirement_base {
+    protected $url;
+
+    /**
+     * Constructor. Normally the class and its subclasses should not be created
+     * directly. Client code should create them via a page_requirements_manager
+     * method like ->js(...).
+     *
+     * @param page_requirements_manager $manager the page_requirements_manager we are associated with.
+     * @param string $url The URL of the thing we are linking to.
+     */
+    protected function __construct(page_requirements_manager $manager, $url) {
+        parent::__construct($manager);
+        $this->url = $url;
+    }
+
+    /**
+     * @return string the HTML needed to satisfy this requirement.
+     */
+    abstract public function get_html();
+}
+
+
+/**
+ * A subclass of @see{linked_requirement} to represent a requried JavaScript file.
+ *
+ * You should not create instances of this class directly. Instead you should
+ * work with a @see{page_requirements_manager} - and probably the only
+ * page_requirements_manager you will ever need is the one at $PAGE->requires.
+ *
+ * The methods ->asap, ->in_head and at_top_of_body() are indented to be used
+ * as a fluid API, so you can say things like
+ *     $PAGE->requires->js('mod/mymod/script.js')->in_head();
+ *
+ * However, by default JavaScript files are included at the end of the HTML.
+ * This is recommended practice because it means that the web browser will only
+ * start loading the javascript files after the rest of the page is loaded, and
+ * that gives the best performance for users.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class required_js extends linked_requirement {
+    /**
+     * Constructor. Normally instances of this class should not be created
+     * directly. Client code should create them via the page_requirements_manager
+     * method ->js(...).
+     *
+     * @param page_requirements_manager $manager the page_requirements_manager we are associated with.
+     * @param string $url The URL of the JavaScript file we are linking to.
+     */
+    public function __construct(page_requirements_manager $manager, $url) {
+        parent::__construct($manager, $url);
+        $this->when = page_requirements_manager::WHEN_AT_END;
+    }
+
+    public function get_html() {
+        return '<script type="text/javascript"  src="' . $this->url . '"></script>' . "\n";
+    }
+
+    /**
+     * Indicate that the link to this JavaScript file should be output as soon as
+     * possible. That is, if this requirement has already been output, this method
+     * does nothing. Otherwise, if the &lt;head> tag has not yet been printed, the link
+     * to this script will be put in &lt;head>. Otherwise, this method returns a
+     * fragment of HTML that the caller is responsible for outputting as soon as
+     * possible. In fact, it is recommended that you only call this function from
+     * an echo statement, like:
+     *     echo $PAGE->requires->js(...)->asap();
+     *
+     * @return string The HTML required to include this JavaScript file. The caller
+     * is responsible for outputting this HTML promptly.
+     */
+    public function asap() {
+        if ($this->is_done()) {
+            return;
+        }
+        if (!$this->manager->is_head_done()) {
+            $this->in_head();
+            return '';
+        }
+        $ouput = $this->get_html();
+        $this->mark_done();
+        return $ouput;
+    }
+
+    /**
+     * Indicate that the link to this JavaScript file should be output in the
+     * &lt;head> section of the HTML. If it too late for this request to be
+     * satisfied, an exception is thrown.
+     */
+    public function in_head() {
+        if ($this->is_done() || $this->when <= page_requirements_manager::WHEN_IN_HEAD) {
+            return;
+        }
+        if ($this->manager->is_head_done()) {
+            throw new coding_exception('Too late to ask for a JavaScript file to be linked to from &lt;head>.');
+        }
+        $this->when = page_requirements_manager::WHEN_IN_HEAD;
+    }
+
+    /**
+     * Indicate that the link to this JavaScript file should be output at the top
+     * of the &lt;body> section of the HTML. If it too late for this request to be
+     * satisfied, an exception is thrown.
+     */
+    public function at_top_of_body() {
+        if ($this->is_done() || $this->when <= page_requirements_manager::WHEN_TOP_OF_BODY) {
+            return;
+        }
+        if ($this->manager->is_top_of_body_done()) {
+            throw new coding_exception('Too late to ask for a JavaScript file to be linked to from the top of &lt;body>.');
+        }
+        $this->when = page_requirements_manager::WHEN_TOP_OF_BODY;
+    }
+}
+
+
+/**
+ * A subclass of @see{linked_requirement} to represent a requried YUI library.
+ *
+ * You should not create instances of this class directly. Instead you should
+ * work with a @see{page_requirements_manager} - and probably the only
+ * page_requirements_manager you will ever need is the one at $PAGE->requires.
+ *
+ * The methods ->asap, ->in_head and at_top_of_body() are indented to be used
+ * as a fluid API, so you can say things like
+ *     $PAGE->requires->yui_lib('autocomplete')->in_head();
+ *
+ * This class (with the help of @see{ajax_resolve_yui_lib}) knows about the
+ * dependancies between the different YUI libraries, and will include all the
+ * other libraries required by the one you ask for. It also knows which YUI
+ * libraries require css files. If the library you ask for requires CSS files,
+ * then you should ask for it before &lt;head> is output, or an exception will
+ * be thrown.
+ *
+ * By default JavaScript files are included at the end of the HTML.
+ * This is recommended practice because it means that the web browser will only
+ * start loading the javascript files after the rest of the page is loaded, and
+ * that gives the best performance for users.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class required_yui_lib extends linked_requirement {
+    protected $jss = array();
+    protected $cssurls;
+
+    /**
+     * Constructor. Normally instances of this class should not be created
+     * directly. Client code should create them via the page_requirements_manager
+     * method ->yui_lib(...).
+     *
+     * @param page_requirements_manager $manager the page_requirements_manager we are associated with.
+     * @param string $libname The name of the YUI library you want. See the array
+     * defined in @see{ajax_resolve_yui_lib} for a list of known libraries.
+     */
+    public function __construct(page_requirements_manager $manager, $libname) {
+        parent::__construct($manager, '');
+        $this->when = page_requirements_manager::WHEN_AT_END;
+
+        list($jsurls, $this->cssurls) = ajax_resolve_yui_lib($libname);
+        foreach ($jsurls as $jsurl) {
+            $this->jss[] = $manager->js($jsurl, true);
+        }
+        foreach ($this->cssurls as $cssurl) {
+            $manager->css($cssurl, true);
+        }
+        if (!empty($this->cssurls)) {
+            global $PAGE;
+            $page->add_body_class('yui-skin-sam');
+        }
+    }
+
+    public function get_html() {
+        // Since we create a required_js for each of our files, that will generate the HTML.
+        return '';
+    }
+
+    /**
+     * Indicate that the link to this YUI library file should be output as soon as
+     * possible. The comment above @see{required_js::asap} applies to this method too.
+     *
+     * @return string The HTML required to include this JavaScript file. The caller
+     * is responsible for outputting this HTML promptly. For example, a good way to
+     * call this method is like
+     *     echo $PAGE->requires->yui_lib(...)->asap();
+     */
+    public function asap() {
+        if ($this->is_done()) {
+            return;
+        }
+
+        if (!$this->manager->is_head_done()) {
+            $this->in_head();
+            return '';
+        }
+
+        $ouput = '';
+        foreach ($this->jss as $requiredjs) {
+            $ouput .= $requiredjs->immediately();
+        }
+        $this->mark_done();
+        return $ouput;
+    }
+
+    /**
+     * Indicate that the links to this  YUI library should be output in the
+     * &lt;head> section of the HTML. If it too late for this request to be
+     * satisfied, an exception is thrown.
+     */
+    public function in_head() {
+        if ($this->is_done() || $this->when <= page_requirements_manager::WHEN_IN_HEAD) {
+            return;
+        }
+
+        if ($this->manager->is_head_done()) {
+            throw new coding_exception('Too late to ask for a YUI library to be linked to from &lt;head>.');
+        }
+
+        $this->when = page_requirements_manager::WHEN_IN_HEAD;
+        foreach ($this->jss as $requiredjs) {
+            $ouput .= $requiredjs->in_head();
+        }
+    }
+
+    /**
+     * Indicate that the links to this YUI library should be output in the
+     * &lt;head> section of the HTML. If it too late for this request to be
+     * satisfied, an exception is thrown.
+     */
+    public function at_top_of_body() {
+        if ($this->is_done() || $this->when <= page_requirements_manager::WHEN_TOP_OF_BODY) {
+            return;
+        }
+
+        if ($this->manager->is_top_of_body_done()) {
+            throw new coding_exception('Too late to ask for a YUI library to be linked to from the top of &lt;body>.');
+        }
+
+        $this->when = page_requirements_manager::WHEN_TOP_OF_BODY;
+        foreach ($this->jss as $requiredjs) {
+            $ouput .= $requiredjs->at_top_of_body();
+        }
+    }
+}
+
+
+/**
+ * A subclass of @see{linked_requirement} to represent a required CSS file.
+ * Of course, all links to CSS files must go in the &lt;head section of the HTML.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class required_css extends linked_requirement {
+    /**
+     * Constructor. Normally instances of this class should not be created
+     * directly. Client code should create them via the page_requirements_manager
+     * method ->css(...).
+     *
+     * @param page_requirements_manager $manager the page_requirements_manager we are associated with.
+     * @param string $url The URL of the CSS file we are linking to.
+     */
+    public function __construct(page_requirements_manager $manager, $url) {
+        parent::__construct($manager, $url);
+        $this->when = page_requirements_manager::WHEN_IN_HEAD;
+    }
+
+    public function get_html() {
+        return '<link rel="stylesheet" type="text/css" href="' . $this->url . '" />' . "\n";;
+    }
+}
+
+
+/**
+ * A subclass of @see{linked_requirement} to represent a skip link.
+ * A skip link is a concept from accessibility. You have some links like
+ * 'Skip to main content' linking to an #maincontent anchor, at the start of the
+ * &lt;body> tag, so that users using assistive technologies like screen readers
+ * can easily get to the main content without having to work their way through
+ * any navigation, blocks, etc. that comes before it in the HTML.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class required_skip_link extends linked_requirement {
+    protected $linktext;
+
+    /**
+     * Constructor. Normally instances of this class should not be created
+     * directly. Client code should create them via the page_requirements_manager
+     * method ->yui_lib(...).
+     *
+     * @param page_requirements_manager $manager the page_requirements_manager we are associated with.
+     * @param string $target the name of the anchor in the page we are linking to.
+     * @param string $linktext the test to use for the link.
+     */
+    public function __construct(page_requirements_manager $manager, $target, $linktext) {
+        parent::__construct($manager, $target);
+        $this->when = page_requirements_manager::WHEN_TOP_OF_BODY;
+        $this->linktext = $linktext;
+    }
+
+    public function get_html() {
+        return '<a class="skip" href="#' . $this->url . '">' . $this->linktext . "</a>\n";
+    }
+}
+
+
+/**
+ * This is the base class for requirements that are JavaScript code.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class required_js_code extends requirement_base {
+
+    /**
+     * Constructor. Normally the class and its subclasses should not be created
+     * directly. Client code should create them via a page_requirements_manager
+     * method like ->js(...).
+     *
+     * @param page_requirements_manager $manager the page_requirements_manager we are associated with.
+     */
+    protected function __construct(page_requirements_manager $manager) {
+        parent::__construct($manager);
+        $this->when = page_requirements_manager::WHEN_AT_END;
+    }
+
+    /**
+     * @return string the JavaScript code needed to satisfy this requirement.
+     */
+    abstract public function get_js_code();
+
+   /**
+     * Indicate that the link to this JavaScript file should be output as soon as
+     * possible. That is, if this requirement has already been output, this method
+     * does nothing. Otherwise, if the &lt;head> tag has not yet been printed, the link
+     * to this script will be put in &lt;head>. Otherwise, this method returns a
+     * fragment of HTML that the caller is responsible for outputting as soon as
+     * possible. In fact, it is recommended that you only call this function from
+     * an echo statement, like:
+     *     echo $PAGE->requires->js(...)->asap();
+     *
+     * @return string The HTML required to include this JavaScript file. The caller
+     * is responsible for outputting this HTML promptly.
+     */
+    public function asap() {
+        if ($this->is_done()) {
+            return;
+        }
+        if (!$this->manager->is_head_done()) {
+            $this->in_head();
+            return '';
+        }
+        $js = $this->get_js_code();
+        $output = ajax_generate_script_tag($js);
+        $this->mark_done();
+        return $output;
+    }
+
+    /**
+     * Indicate that the link to this JavaScript file should be output in the
+     * &lt;head> section of the HTML. If it too late for this request to be
+     * satisfied, an exception is thrown.
+     */
+    public function in_head() {
+        if ($this->is_done() || $this->when <= page_requirements_manager::WHEN_IN_HEAD) {
+            return;
+        }
+        if ($this->manager->is_head_done()) {
+            throw new coding_exception('Too late to ask for some JavaScript code to be output in &lt;head>.');
+        }
+        $this->when = page_requirements_manager::WHEN_IN_HEAD;
+    }
+
+    /**
+     * Indicate that the link to this JavaScript file should be output at the top
+     * of the &lt;body> section of the HTML. If it too late for this request to be
+     * satisfied, an exception is thrown.
+     */
+    public function at_top_of_body() {
+        if ($this->is_done() || $this->when <= page_requirements_manager::WHEN_TOP_OF_BODY) {
+            return;
+        }
+        if ($this->manager->is_top_of_body_done()) {
+            throw new coding_exception('Too late to ask for some JavaScript code to be output at the top of &lt;body>.');
+        }
+        $this->when = page_requirements_manager::WHEN_TOP_OF_BODY;
+    }
+}
+
+
+/**
+ * This class represents a JavaScript function that must be called from the HTML
+ * page. By default the call will be made at the end of the page, but you can
+ * chage that using the ->asap, ->in_head, etc. methods.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class required_js_function_call extends required_js_code {
+    protected $function;
+    protected $arguments;
+
+    /**
+     * Constructor. Normally the class and its subclasses should not be created
+     * directly. Client code should create them via the page_requirements_manager
+     * method ->js_function_call(...).
+     *
+     * @param page_requirements_manager $manager the page_requirements_manager we are associated with.
+     * @param string $function the name of the JavaScritp function to call.
+     *      Can be a compound name like 'YAHOO.util.Event.addListener'.
+     * @param array $arguments and array of arguments to be passed to the function.
+     *      When generating the function call, this will be escaped using json_encode,
+     *      so passing objects and arrays should work.
+     */
+    public function __construct(page_requirements_manager $manager, $function, $arguments) {
+        parent::__construct($manager);
+        $this->function = $function;
+        $this->arguments = $arguments;
+    }
+
+    public function get_js_code() {
+        $quotedargs = array();
+        foreach ($this->arguments as $arg) {
+            $quotedargs[] = json_encode($arg);
+        }
+        return $this->function . '(' . implode(', ', $quotedargs) . ");\n";
+    }
+
+    /**
+     * Indicate that this function should be called in YUI's onDomReady event.
+     *
+     * Not that this is probably not necessary most of the time. Just having the
+     * function call at the end of the HTML should normally be sufficient.
+     */
+    public function on_dom_ready() {
+        if ($this->is_done() || $this->when < page_requirements_manager::WHEN_AT_END) {
+            return;
+        }
+        $this->manager->yui_lib('event');
+        $this->when = page_requirements_manager::WHEN_ON_DOM_READY;
+    }
+}
+
+
+/**
+ * This class represents some data from PHP that needs to be made available in a
+ * global JavaScript variable. By default the data will be output at the end of
+ * the page, but you can chage that using the ->asap, ->in_head, etc. methods.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class required_data_for_js extends required_js_code {
+    protected $variable;
+    protected $data;
+
+    /**
+     * Constructor. Normally the class and its subclasses should not be created
+     * directly. Client code should create them via the page_requirements_manager
+     * method ->data_for_js(...).
+     *
+     * @param page_requirements_manager $manager the page_requirements_manager we are associated with.
+     * @param string $variable the the name of the JavaScript variable to assign the data to.
+     *      Will probably work if you use a compound name like 'mybuttons.button[1]', but this
+     *      should be considered an experimental feature.
+     * @param mixed $data The data to pass to JavaScript. This will be escaped using json_encode,
+     *      so passing objects and arrays should work.
+     */
+    public function __construct(page_requirements_manager $manager, $variable, $data) {
+        parent::__construct($manager);
+        $this->variable = $variable;
+        $this->data = json_encode($data);
+        // json_encode immediately, so that if $data is an object (and therefore was
+        // passed in by reference) we get the data at the time the call was made, and
+        // not whatever the data happened to be when this is output.
+    }
+
+    public function get_js_code() {
+        $prefix = 'var ';
+        if (strpos($this->variable, '.') || strpos($this->variable, '[')) {
+            $prefix = '';
+        }
+        return $prefix . $this->variable . ' = ' . $this->data . ";\n";
+    }
+}
+
+
+/**
+ * Generate a script tag containing the the specified code.
+ *
+ * @param string $js the JavaScript code
+ * @return string HTML, the code wrapped in <script> tags.
+ */
+function ajax_generate_script_tag($js) {
+    return '<script type="text/javascript">' . "\n//<![CDATA[\n" .
+            $js . "//]]>\n</script>\n";
+}
+
+
+/**
+ * Given the name of a YUI library, return a list of the .js and .css files that
+ * it requries.
+ *
+ * This method takes note of the $CFG->useexternalyui setting.
+ *
+ * If $CFG->debug is set to DEBUG_DEVELOPER then this method will return links to
+ * the -debug version of the YUI files, otherwise it will return links to the -min versions.
+ *
+ * @param string $libname the name of a YUI library, for example 'autocomplete'.
+ * @return array with two elementes. The first is an array of the JavaScript URLs
+ *      that must be loaded to make this library work, in the order they should be
+ *      loaded. The second element is a (possibly empty) list of CSS files that
+ *      need to be loaded.
+ */
+function ajax_resolve_yui_lib($libname) {
+    global $CFG;
+
+    // Note, we always use yahoo-dom-event, even if we are only asked for part of it.
+    // because another part of the code may later ask for other bits. It is easier, and
+    // not very inefficient, just to always use (and get browsers to cache) the combined file.
+    static $translatelist = array(
+        'yahoo' => 'yahoo-dom-event',
+        'animation' => array('yahoo-dom-event', 'animation'),
+        'autocomplete' => array(
+                'js' => array('yahoo-dom-event', 'datasource', 'autocomplete'),
+                'css' => array('autocomplete/assets/skins/sam/autocomplete.css')),
+        'button' => array(
+                'js' => array('yahoo-dom-event', 'element', 'button'),
+                'css' => array('button/assets/skins/sam/button.css')),
+        'calendar' => array(
+                'js' => array('yahoo-dom-event', 'calendar'),
+                'css' => array('calendar/assets/skins/sam/calendar.css')),
+        'carousel' => array(
+                'js' => array('yahoo-dom-event', 'element', 'carousel'),
+                'css' => array('carousel/assets/skins/sam/carousel.css')),
+        'charts' => array('yahoo-dom-event', 'element', 'datasource', 'json', 'charts'),
+        'colorpicker' => array(
+                'js' => array('utilities', 'slider', 'colorpicker'),
+                'css' => array('colorpicker/assets/skins/sam/colorpicker.css')),
+        'connection' => array('yahoo-dom-event', 'connection'),
+        'container' => array(
+                'js' => array('yahoo-dom-event', 'container'),
+                'css' => array('container/assets/skins/sam/container.css')),
+        'cookie' => array('yahoo-dom-event', 'cookie'),
+        'datasource' => array('yahoo-dom-event', 'datasource'),
+        'datatable' => array(
+                'js' => array('yahoo-dom-event', 'element', 'datasource', 'datatable'),
+                'css' => array('datatable/assets/skins/sam/datatable.css')),
+        'dom' => 'yahoo-dom-event',
+        'dom-event' => 'yahoo-dom-event',
+        'dragdrop' => array('yahoo-dom-event', 'dragdrop'),
+        'editor' => array(
+                'js' => array('yahoo-dom-event', 'element', 'container', 'menu', 'button', 'editor'),
+                'css' => array('assets/skins/sam/skin.css')),
+        'element' => array('yahoo-dom-event', 'element'),
+        'event' => 'yahoo-dom-event',
+        'get' => array('yahoo-dom-event', 'get'),
+        'history' => array('yahoo-dom-event', 'history'),
+        'imagecropper' => array(
+                'js' => array('yahoo-dom-event', 'dragdrop', 'element', 'resize', 'imagecropper'),
+                'css' => array('assets/skins/sam/resize.css', 'assets/skins/sam/imagecropper.css')),
+        'imageloader' => array('yahoo-dom-event', 'imageloader'),
+        'json' => array('yahoo-dom-event', 'json/json'),
+        'layout' => array(
+                'js' => array('yahoo-dom-event', 'dragdrop', 'element', 'layout'),
+                'css' => array('reset-fonts-grids/reset-fonts-grids.css', 'assets/skins/sam/layout.css')),
+        'logger' => array(
+                'js' => array('yahoo-dom-event', 'logger'),
+                'css' => array('logger/assets/skins/sam/logger.css')),
+        'menu' => array(
+                'js' => array('yahoo-dom-event', 'container', 'menu'),
+                'css' => array('menu/assets/skins/sam/menu.css')),
+        'paginator' => array(
+                'js' => array('yahoo-dom-event', 'element', 'paginator'),
+                'css' => array('paginator/assets/skins/sam/paginator.css')),
+        'profiler' => array('yahoo-dom-event', 'profiler'),
+        'profilerviewer' => array('yuiloader-dom-event', 'element', 'profiler', 'profilerviewer'),
+        'resize' => array(
+                'js' => array('yahoo-dom-event', 'dragdrop', 'element', 'resize'),
+                'css' => array('assets/skins/sam/resize.css')),
+        'selector' => array('yahoo-dom-event', 'selector'),
+        'simpleeditor' => array(
+                'js' => array('yahoo-dom-event', 'element', 'container', 'simpleeditor'),
+                'css' => array('assets/skins/sam/skin.css')),
+        'slider' => array('yahoo-dom-event', 'gragdrop', 'slider'),
+        'stylesheet' => array('yahoo-dom-event', 'stylesheet'),
+        'tabview' => array(
+                'js' => array('yahoo-dom-event', 'element', 'tabview'),
+                'css' => array('assets/skins/sam/skin.css')),
+        'treeview' => array(
+                'js' => array('yahoo-dom-event', 'treeviewed'),
+                'css' => array('treeview/assets/skins/sam/treeview.css')),
+        'uploader' => array('yahoo-dom-event', 'element', 'uploader'),
+        'utilities' => array('yahoo-dom-event', 'connection', 'animation', 'dragdrop', 'element', 'get'),
+        'yuiloader' => 'yuiloader',
+        'yuitest' => array(
+                'js' => array('yahoo-dom-event', 'logger', 'yuitest'),
+                'css' => array('logger/assets/logger.css', 'yuitest/assets/testlogger.css')),
+    );
+    if (!isset($translatelist[$libname])) {
+        throw new coding_exception('Unknown YUI library ' . $libname);
+    }
+
+    $data = $translatelist[$libname];
+    if (!is_array($data)) {
+        $jsnames = array($data);
+        $cssfiles = array();
+    } else if (isset($data['js']) && isset($data['css'])) {
+        $jsnames = $data['js'];
+        $cssfiles = $data['css'];
+    } else {
+        $jsnames = $data;
+        $cssfiles = array();
+    }
+
+    $debugging = debugging('', DEBUG_DEVELOPER);
+    if ($debugging) {
+        $suffix = '-debug.js';
+    } else {
+        $suffix = '-min.js';
+    }
+    $libpath = $CFG->httpswwwroot . '/lib/yui/';
+
+    $externalyui = !empty($CFG->useexternalyui);
+    if ($externalyui) {
+        include($CFG->libdir.'/yui/version.php'); // Sets $yuiversion.
+        $libpath = 'http://yui.yahooapis.com/' . $yuiversion . '/build/';
+    }
+
+    $jsurls = array();
+    foreach ($jsnames as $js) {
+        if ($js == 'yahoo-dom-event') {
+            if ($debugging) {
+                $jsurls[] = $libpath . 'yahoo/yahoo' . $suffix;
+                $jsurls[] = $libpath . 'dom/dom' . $suffix;
+                $jsurls[] = $libpath . 'event/event' . $suffix;
+            } else {
+                $jsurls[] = $jsurls[] = $libpath . $js . '/' . $js . '.js';
+            }
+        } else {
+            $jsurls[] = $libpath . $js . '/' . $js . $suffix;
+        }
+    }
+
+    $cssurls = array();
+    foreach ($cssfiles as $css) {
+        $cssurls[] = $libpath . $css;
+    }
+
+    return array($jsurls, $cssurls);
+}
 
 /**
  * Get the path to a JavaScript library.
index 6052d87b62189103cf56da2e0e1732ff5a8ea473..003e19b7ac6250c5092e5ec89ad89eee08baf06b 100644 (file)
-<?php // $Id$
-
-///////////////////////////////////////////////////////////////////////////
-//                                                                       //
-// NOTICE OF COPYRIGHT                                                   //
-//                                                                       //
-// Moodle - Modular Object-Oriented Dynamic Learning Environment         //
-//          http://moodle.org                                            //
-//                                                                       //
-// Copyright (C) 1999 onwards Martin Dougiamas  http://dougiamas.com     //
-//                                                                       //
-// This program 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 2 of the License, or     //
-// (at your option) any later version.                                   //
-//                                                                       //
-// This program 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:                          //
-//                                                                       //
-//          http://www.gnu.org/copyleft/gpl.html                         //
-//                                                                       //
-///////////////////////////////////////////////////////////////////////////
+<?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/>.
+
 
 /**
  * Unit tests for (some of) ../ajaxlib.php.
  *
- * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
- * @package moodlecore
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
 if (!defined('MOODLE_INTERNAL')) {
     die('Direct access to this script is forbidden.');    ///  It must be included from a Moodle page
 }
-
 require_once($CFG->libdir . '/ajax/ajaxlib.php');
 
+
+/**
+ * Helper class, adds some useful stuff to UnitTestCase that the other test cases
+ * classes in this file can benefit from.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+abstract class ajaxlib_unit_test_base extends UnitTestCase {
+    protected $requires;
+
+    public function setUp() {
+        parent::setUp();
+        $this->requires = new page_requirements_manager();
+    }
+
+    public function tearDown() {
+        $this->requires = null;
+        parent::tearDown();
+    }
+
+    public function assertContains($actual, $expectedsubstring) {
+        $this->assertNotIdentical(strpos($actual, (string) $expectedsubstring), false,
+                "[$actual] does not containg the substring [$expectedsubstring].");
+    }
+}
+
+
+/**
+ * Unit tests for the requriement_base base class. Don't be confused by the
+ * fact we are using a specific subclass to test with.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class requriement_base_test extends ajaxlib_unit_test_base {
+    protected $classname = 'required_css';
+
+    public function test_not_done_initially() {
+        $requirement = new $this->classname($this->requires, '');
+        $this->assertFalse($requirement->is_done());
+    }
+
+    public function test_done_when_marked() {
+        $requirement = new $this->classname($this->requires, '');
+        $requirement->mark_done();
+        $this->assertTrue($requirement->is_done());
+    }
+}
+
+
+/**
+ * Unit tests for the required_css class.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class required_css_test extends ajaxlib_unit_test_base {
+    protected $cssurl = 'http://example.com/styles.css';
+
+    public function test_when() {
+        $requirement = new required_css($this->requires, $this->cssurl);
+        $this->assertEqual($requirement->get_when(), page_requirements_manager::WHEN_IN_HEAD);
+    }
+
+    public function test_round_trip_url_to_html() {
+        $requirement = new required_css($this->requires, $this->cssurl);
+        $html = $requirement->get_html();
+        $this->assertContains($html, $this->cssurl);
+        $this->assertContains($html, '<link ');
+        $this->assertContains($html, 'type="text/css"');
+    }
+}
+
+
+/**
+ * Unit tests for the required_skip_link class.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class required_skip_link_test extends ajaxlib_unit_test_base {
+    protected $target = 'skiptarget';
+    protected $linktext = 'Skip to link text';
+
+    public function test_when() {
+        $requirement = new required_skip_link($this->requires, $this->target, $this->linktext);
+        $this->assertEqual($requirement->get_when(), page_requirements_manager::WHEN_TOP_OF_BODY);
+    }
+
+    public function test_round_trip_to_html() {
+        $requirement = new required_skip_link($this->requires, $this->target, $this->linktext);
+        $html = $requirement->get_html();
+        $this->assertContains($html, '>' . $this->linktext . '</a>');
+        $this->assertContains($html, '<a ');
+        $this->assertContains($html, 'class="skip"');
+        $this->assertContains($html, 'href="#' . $this->target . '"');
+    }
+}
+
+
+/**
+ * Unit tests for the required_js_code class. Once again we are tesing the
+ * behaviour of an abstract class by creating instances one particular subclass.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class required_js_code_test extends ajaxlib_unit_test_base {
+    protected $classname = 'required_data_for_js';
+
+    public function test_when() {
+        $requirement = new $this->classname($this->requires, '', '');
+        $this->assertEqual($requirement->get_when(), page_requirements_manager::WHEN_AT_END);
+    }
+
+    public function test_setting_when_to_head() {
+        $requirement = new $this->classname($this->requires, '', '');
+        $requirement->in_head();
+        $this->assertEqual($requirement->get_when(), page_requirements_manager::WHEN_IN_HEAD);
+    }
+
+    public function test_setting_when_to_top_of_body() {
+        $requirement = new $this->classname($this->requires, '', '');
+        $requirement->at_top_of_body();
+        $this->assertEqual($requirement->get_when(), page_requirements_manager::WHEN_TOP_OF_BODY);
+    }
+
+    public function test_asap_before_head_is_head() {
+        $requirement = new $this->classname($this->requires, '', '');
+        $this->assertEqual($requirement->asap(), '');
+        $this->assertEqual($requirement->get_when(), page_requirements_manager::WHEN_IN_HEAD);
+    }
+
+    public function test_asap_after_head() {
+        $requirement = new $this->classname($this->requires, '', '');
+        $this->requires->get_head_code();
+
+        $this->assertNotEqual($requirement->asap(), '');
+        $this->assertTrue($requirement->is_done());
+    }
+
+    public function test_in_head_when_too_late_throws_exception() {
+        $requirement = new $this->classname($this->requires, '', '');
+        $this->requires->get_head_code();
+
+        $this->expectException();
+        $requirement->in_head();
+    }
+
+    public function test_in_head_when_too_late_no_exception_if_done() {
+        $requirement = new $this->classname($this->requires, '', '');
+        $requirement->mark_done();
+        $this->requires->get_head_code();
+
+        $requirement->in_head();
+        $this->pass('No exception thrown as expected.');
+    }
+
+    public function test_body_top_when_too_late_throws_exception() {
+        $requirement = new $this->classname($this->requires, '', '');
+        $this->requires->get_top_of_body_code();
+
+        $this->expectException();
+        $requirement->at_top_of_body();
+    }
+
+    public function test_body_top_when_too_late_no_exception_if_done() {
+        $requirement = new $this->classname($this->requires, '', '');
+        $requirement->mark_done();
+        $this->requires->get_top_of_body_code();
+
+        $requirement->at_top_of_body();
+        $this->pass('No exception thrown as expected.');
+    }
+
+    public function test_body_top_does_not_override_in_head() {
+        $requirement = new $this->classname($this->requires, '', '');
+
+        $requirement->in_head();
+        $requirement->at_top_of_body();
+        $this->assertEqual($requirement->get_when(), page_requirements_manager::WHEN_IN_HEAD);
+    }
+}
+
+
+/**
+ * Unit tests for the required_js_function_call class.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class required_js_function_call_test extends ajaxlib_unit_test_base {
+    protected $function = 'object.method';
+    protected $params = array('arg1', 2);
+
+    public function test_round_trip_to_js_code() {
+        $requirement = new required_js_function_call($this->requires, $this->function, $this->params);
+        $js = $requirement->get_js_code();
+        $this->assertContains($js, $this->function . '(');
+        $this->assertContains($js, $this->params[0]);
+        $this->assertContains($js, $this->params[1]);
+    }
+
+    public function test_setting_when_on_dom_ready() {
+        $requirement = new required_js_function_call($this->requires, $this->function, $this->params);
+        $requirement->on_dom_ready();
+        $this->assertEqual($requirement->get_when(), page_requirements_manager::WHEN_ON_DOM_READY);
+    }
+
+    public function test_body_top_does_not_override_earlier_value() {
+        $requirement = new required_js_function_call($this->requires, $this->function, $this->params);
+        $requirement->at_top_of_body();
+        $requirement->on_dom_ready();
+        $this->assertEqual($requirement->get_when(), page_requirements_manager::WHEN_TOP_OF_BODY);
+    }
+}
+
+
+/**
+ * Unit tests for the required_data_for_js class.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class required_data_for_js_test extends ajaxlib_unit_test_base {
+    public function test_round_trip_to_js_code() {
+        $requirement = new required_data_for_js($this->requires, 'varname', 'Value');
+        $js = $requirement->get_js_code();
+        $this->assertContains($js, 'var varname =');
+        $this->assertContains($js, 'Value');
+    }
+
+    public function test_no_var_for_complex_varname_1() {
+        $requirement = new required_data_for_js($this->requires, 'obj.field', 'Value');
+        $js = $requirement->get_js_code();
+        $this->assertPattern('/^obj\\.field =/', $js);
+        $this->assertContains($js, 'Value');
+    }
+
+    public function test_no_var_for_complex_varname_2() {
+        $requirement = new required_data_for_js($this->requires, 'arry[0]', 'Value');
+        $js = $requirement->get_js_code();
+        $this->assertPattern('/^arry\\[0\\] =/', $js);
+        $this->assertContains($js, 'Value');
+    }
+}
+
+
+/**
+ * Unit tests for the page_requirements_manager class.
+ *
+ * @copyright 2009 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class page_requirements_manager_test extends ajaxlib_unit_test_base {
+
+    public function test_outputting_head_marks_it_done() {
+        $this->requires->get_head_code();
+        $this->assertTrue($this->requires->is_head_done());
+    }
+
+    public function test_outputting_body_top_marks_it_done() {
+        $this->requires->get_top_of_body_code();
+        $this->assertTrue($this->requires->is_top_of_body_done());
+    }
+
+    public function test_requiring_css() {
+        global $CFG;
+        $cssfile = 'theme/standard/styles_layout.css'; // Just needs to be a CSS file that exists.
+        $this->requires->css($cssfile);
+
+        $html = $this->requires->get_head_code();
+        $this->assertContains($html, $CFG->httpswwwroot . '/' . $cssfile);
+    }
+
+    public function test_nonexistant_css_throws_exception() {
+        $cssfile = 'css/file/that/does/not/exist.css';
+
+        $this->expectException();
+        $this->requires->css($cssfile);
+    }
+
+    public function test_requiring_js() {
+        global $CFG;
+        $jsfile = 'lib/javascript-static.js'; // Just needs to be a JS file that exists.
+        $this->requires->js($jsfile);
+
+        $html = $this->requires->get_end_code();
+        $this->assertContains($html, $CFG->httpswwwroot . '/' . $jsfile);
+    }
+
+    public function test_nonexistant_js_throws_exception() {
+        $cssfile = 'js/file/that/does/not/exist.js';
+
+        $this->expectException();
+        $this->requires->js($cssfile);
+    }
+
+    public function test_requiring_skip_link() {
+        $this->requires->skip_link_to('target', 'Link text');
+
+        $html = $this->requires->get_top_of_body_code();
+        $this->assertContains($html, 'target');
+        $this->assertContains($html, 'Link text');
+    }
+
+    public function test_requiring_js_function_call() {
+        $this->requires->js_function_call('fn');
+
+        $html = $this->requires->get_end_code();
+        $this->assertContains($html, 'fn()');
+    }
+
+    public function test_requiring_string_for_js() {
+        $this->requires->string_for_js('course', 'moodle');
+
+        $html = $this->requires->get_end_code();
+        $this->assertContains($html, 'mstr');
+        $this->assertContains($html, 'course');
+        $this->assertContains($html, 'moodle');
+    }
+
+    public function test_repeat_string_different_a_throws_exception() {
+        $this->requires->string_for_js('added', 'moodle', 'this');
+        $this->expectException();
+        $this->requires->string_for_js('added', 'moodle', 'that');
+    }
+
+    public function test_repeat_string_same_a_is_ok() {
+        $this->requires->string_for_js('added', 'moodle', 'same$a');
+        $this->requires->string_for_js('added', 'moodle', 'same$a');
+        $this->pass('No exception thrown as expected.');
+    }
+
+    public function test_requiring_data_for_js() {
+        $this->requires->data_for_js('varname', 'Value')->at_top_of_body();
+
+        $html = $this->requires->get_top_of_body_code();
+        $this->assertContains($html, 'varname');
+        $this->assertContains($html, 'Value');
+    }
+
+    public function test_requiring_js_function_call_on_dom_ready() {
+        $this->requires->js_function_call('fn')->on_dom_ready();
+
+        $html = $this->requires->get_end_code();
+        $this->assertPattern('/<script.*src=".*event.*\.js"/', $html);
+        $this->assertContains($html, 'YAHOO.util.Event.onDOMReady');
+        $this->assertContains($html, 'fn()');
+    }
+}
+
+
 /**
- * Unit tests of mathslib wrapper and underlying EvalMath library.
+ * Unit tests for ../ajaxlib.php functions.
  *
- * @author Petr Skoda (skodak)
- * @version $Id$
+ * @copyright 2008 Tim Hunt
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class ajax_test extends UnitTestCase {
+class ajax_test extends ajaxlib_unit_test_base {
+
+    function test_ajax_generate_script_tag() {
+        $html = ajax_generate_script_tag('');
+        $this->assertContains($html, '<script type="text/javascript">');
+        $this->assertContains($html, '<![CDATA[');
+        $this->assertContains($html, ']]>');
+        $this->assertContains($html, '</script>');
+    }
+
     function test_ajax_get_lib() {
         global $CFG;
         $olddebug = $CFG->debug;
index 4bcc47ba6e75d5193e16368cae07391aa13ac217..5f62bafaa6d5090463dea43c488bfddec7e40659 100644 (file)
@@ -97,6 +97,8 @@ class moodle_page {
 
     protected $_blocks = null;
 
+    protected $_requires = null;
+
     protected $_blockseditingcap = 'moodle/site:manageblocks';
 
     protected $_othereditingcaps = array();
@@ -291,6 +293,19 @@ class moodle_page {
         return $this->_blocks;
     }
 
+    /**
+     * Please do not call this method directly, use the ->blocks syntax. @see __get().
+     * @return blocks_manager the blocks manager object for this page.
+     */
+    public function get_requires() {
+        global $CFG;
+        if (is_null($this->_requires)) {
+            $this->_requires = new page_requirements_manager();
+            setup_core_javascript($this->_requires);
+        }
+        return $this->_requires;
+    }
+
     /**
      * PHP overloading magic to make the $PAGE->course syntax work by redirecting
      * it to the corresponding $PAGE->get_course() method if there is one, and