Merge branch 'MDL-32657-master-1' of git://git.luns.net.uk/moodle
[moodle.git] / lib / outputrequirementslib.php
blob8dc60055c17195d920389127f40bded7c75acac1
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 /**
18 * Library functions to facilitate the use of JavaScript in Moodle.
20 * Note: you can find history of this file in lib/ajax/ajaxlib.php
22 * @copyright 2009 Tim Hunt, 2010 Petr Skoda
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 * @package core
25 * @category output
28 defined('MOODLE_INTERNAL') || die();
30 /**
31 * This class tracks all the things that are needed by the current page.
33 * Normally, the only instance of this class you will need to work with is the
34 * one accessible via $PAGE->requires.
36 * Typical usage would be
37 * <pre>
38 * $PAGE->requires->js_init_call('M.mod_forum.init_view');
39 * </pre>
41 * It also supports obsoleted coding style withouth YUI3 modules.
42 * <pre>
43 * $PAGE->requires->css('/mod/mymod/userstyles.php?id='.$id); // not overridable via themes!
44 * $PAGE->requires->js('/mod/mymod/script.js');
45 * $PAGE->requires->js('/mod/mymod/small_but_urgent.js', true);
46 * $PAGE->requires->js_function_call('init_mymod', array($data), true);
47 * </pre>
49 * There are some natural restrictions on some methods. For example, {@link css()}
50 * can only be called before the <head> tag is output. See the comments on the
51 * individual methods for details.
53 * @copyright 2009 Tim Hunt, 2010 Petr Skoda
54 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
55 * @since Moodle 2.0
56 * @package core
57 * @category output
59 class page_requirements_manager {
61 /**
62 * @var array List of string available from JS
64 protected $stringsforjs = array();
66 /**
67 * @var array List of JS variables to be initialised
69 protected $jsinitvariables = array('head'=>array(), 'footer'=>array());
71 /**
72 * @var array Included JS scripts
74 protected $jsincludes = array('head'=>array(), 'footer'=>array());
76 /**
77 * @var array List of needed function calls
79 protected $jscalls = array('normal'=>array(), 'ondomready'=>array());
81 /**
82 * @var array List of skip links, those are needed for accessibility reasons
84 protected $skiplinks = array();
86 /**
87 * @var array Javascript code used for initialisation of page, it should
88 * be relatively small
90 protected $jsinitcode = array();
92 /**
93 * @var array of moodle_url Theme sheets, initialised only from core_renderer
95 protected $cssthemeurls = array();
97 /**
98 * @var array of moodle_url List of custom theme sheets, these are strongly discouraged!
99 * Useful mostly only for CSS submitted by teachers that is not part of the theme.
101 protected $cssurls = array();
104 * @var array List of requested event handlers
106 protected $eventhandlers = array();
109 * @var array Extra modules
111 protected $extramodules = array();
114 * @var bool Flag indicated head stuff already printed
116 protected $headdone = false;
119 * @var bool Flag indicating top of body already printed
121 protected $topofbodydone = false;
124 * @var YAHOO_util_Loader YUI PHPLoader instance responsible for YUI2 loading
125 * from PHP only
127 protected $yui2loader;
130 * @var stdClass YUI PHPLoader instance responsible for YUI3 loading from PHP only
132 protected $yui3loader;
135 * @var stdClass YUI loader information for YUI3 loading from javascript
137 protected $M_yui_loader;
140 * @var array Some config vars exposed in JS, please no secret stuff there
142 protected $M_cfg;
145 * @var array Stores debug backtraces from when JS modules were included in the page
147 protected $debug_moduleloadstacktraces = array();
150 * Page requirements constructor.
152 public function __construct() {
153 global $CFG;
155 require_once("$CFG->libdir/yui/phploader/phploader/loader.php");
157 $this->yui3loader = new stdClass();
158 $this->yui2loader = new YAHOO_util_Loader($CFG->yui2version);
160 // set up some loader options
161 if (debugging('', DEBUG_DEVELOPER)) {
162 $this->yui3loader->filter = YUI_RAW; // for more detailed logging info use YUI_DEBUG here
163 $this->yui2loader->filter = YUI_RAW; // for more detailed logging info use YUI_DEBUG here
164 $this->yui2loader->allowRollups = false;
165 } else {
166 $this->yui3loader->filter = null;
167 $this->yui2loader->filter = null;
169 if (!empty($CFG->useexternalyui) and strpos($CFG->httpswwwroot, 'https:') !== 0) {
170 $this->yui3loader->base = 'http://yui.yahooapis.com/' . $CFG->yui3version . '/build/';
171 $this->yui2loader->base = 'http://yui.yahooapis.com/' . $CFG->yui2version . '/build/';
172 $this->yui3loader->comboBase = 'http://yui.yahooapis.com/combo?';
173 $this->yui2loader->comboBase = 'http://yui.yahooapis.com/combo?';
174 } else {
175 $this->yui3loader->base = $CFG->httpswwwroot . '/lib/yui/'. $CFG->yui3version . '/build/';
176 $this->yui2loader->base = $CFG->httpswwwroot . '/lib/yui/'. $CFG->yui2version . '/build/';
177 $this->yui3loader->comboBase = $CFG->httpswwwroot . '/theme/yui_combo.php?';
178 $this->yui2loader->comboBase = $CFG->httpswwwroot . '/theme/yui_combo.php?';
181 // enable combo loader? this significantly helps with caching and performance!
182 $this->yui3loader->combine = !empty($CFG->yuicomboloading);
183 $this->yui2loader->combine = !empty($CFG->yuicomboloading);
185 if (empty($CFG->cachejs)) {
186 $jsrev = -1;
187 } else if (empty($CFG->jsrev)) {
188 $jsrev = 1;
189 } else {
190 $jsrev = $CFG->jsrev;
193 // set up JS YUI loader helper object
194 $this->M_yui_loader = new stdClass();
195 $this->M_yui_loader->base = $this->yui3loader->base;
196 $this->M_yui_loader->comboBase = $this->yui3loader->comboBase;
197 $this->M_yui_loader->combine = $this->yui3loader->combine;
198 $this->M_yui_loader->filter = (string)$this->yui3loader->filter;
199 $this->M_yui_loader->insertBefore = 'firstthemesheet';
200 $this->M_yui_loader->modules = array();
201 $this->M_yui_loader->groups = array(
202 'moodle' => array(
203 'name' => 'moodle',
204 'base' => $CFG->httpswwwroot . '/theme/yui_combo.php?moodle/'.$jsrev.'/',
205 'comboBase' => $CFG->httpswwwroot . '/theme/yui_combo.php?',
206 'combine' => $this->yui3loader->combine,
207 'filter' => '',
208 'ext' => false,
209 'root' => 'moodle/'.$jsrev.'/', // Add the rev to the root path so that we can control caching
210 'patterns' => array(
211 'moodle-' => array(
212 'group' => 'moodle',
213 'configFn' => '@MOODLECONFIGFN@'
215 'root' => 'moodle'
218 'local' => array(
219 'name' => 'gallery',
220 'base' => $CFG->wwwroot.'/lib/yui/gallery/',
221 'comboBase' => $CFG->httpswwwroot . '/theme/yui_combo.php?',
222 'combine' => $this->yui3loader->combine,
223 'filter' => $this->M_yui_loader->filter,
224 'ext' => false,
225 'root' => 'gallery/',
226 'patterns' => array(
227 'gallery-' => array(
228 'group' => 'gallery',
229 'configFn' => '@GALLERYCONFIGFN@',
231 'root' => 'gallery'
235 $this->add_yui2_modules(); // adds loading info for all YUI2 modules
236 $this->js_module($this->find_module('core_filepicker'));
237 $this->js_module($this->find_module('core_dock'));
242 * This method adds yui2 modules into the yui3 JS loader so that they can
243 * be easily included for use in JavaScript.
245 protected function add_yui2_modules() {
246 //note: this function is definitely not perfect, because
247 // it adds tons of markup into each page, but it can be
248 // abstracted into separate JS file with proper headers
249 global $CFG;
251 $GLOBALS['yui_current'] = array();
252 require($CFG->libdir.'/yui/phploader/lib/meta/config_'.$CFG->yui2version.'.php');
253 $info = $GLOBALS['yui_current'];
254 unset($GLOBALS['yui_current']);
256 if (empty($CFG->yuicomboloading)) {
257 $urlbase = $this->yui2loader->base;
258 } else {
259 $urlbase = $this->yui2loader->comboBase.$CFG->yui2version.'/build/';
262 $modules = array();
263 $ignored = array(); // list of CSS modules that are not needed
264 foreach ($info['moduleInfo'] as $name => $module) {
265 if ($module['type'] === 'css') {
266 $ignored[$name] = true;
267 } else {
268 $modules['yui2-'.$name] = $module;
271 foreach ($modules as $name=>$module) {
272 $module['fullpath'] = $urlbase.$module['path']; // fix path to point to correct location
273 unset($module['path']);
274 unset($module['skinnable']); // we load all YUI2 css automatically, this prevents weird missing css loader problems
275 foreach(array('requires', 'optional', 'supersedes') as $fixme) {
276 if (!empty($module[$fixme])) {
277 $fixed = false;
278 foreach ($module[$fixme] as $key=>$dep) {
279 if (isset($ignored[$dep])) {
280 unset($module[$fixme][$key]);
281 $fixed = true;
282 } else {
283 $module[$fixme][$key] = 'yui2-'.$dep;
286 if ($fixed) {
287 $module[$fixme] = array_merge($module[$fixme]); // fix keys
291 $this->M_yui_loader->modules[$name] = $module;
292 if (debugging('', DEBUG_DEVELOPER)) {
293 if (!array_key_exists($name, $this->debug_moduleloadstacktraces)) {
294 $this->debug_moduleloadstacktraces[$name] = array();
296 $this->debug_moduleloadstacktraces[$name][] = format_backtrace(debug_backtrace());
302 * Initialise with the bits of JavaScript that every Moodle page should have.
304 * @param moodle_page $page
305 * @param core_renderer $renderer
307 protected function init_requirements_data(moodle_page $page, core_renderer $renderer) {
308 global $CFG;
310 // JavaScript should always work with $CFG->httpswwwroot rather than $CFG->wwwroot.
311 // Otherwise, in some situations, users will get warnings about insecure content
312 // on secure pages from their web browser.
314 $this->M_cfg = array(
315 'wwwroot' => $CFG->httpswwwroot, // Yes, really. See above.
316 'sesskey' => sesskey(),
317 'loadingicon' => $renderer->pix_url('i/loading_small', 'moodle')->out(false),
318 'themerev' => theme_get_revision(),
319 'theme' => $page->theme->name,
320 'jsrev' => ((empty($CFG->cachejs) or empty($CFG->jsrev)) ? -1 : $CFG->jsrev),
322 if (debugging('', DEBUG_DEVELOPER)) {
323 $this->M_cfg['developerdebug'] = true;
324 $this->yui2_lib('logger');
327 // accessibility stuff
328 $this->skip_link_to('maincontent', get_string('tocontent', 'access'));
330 // to be removed soon
331 $this->yui2_lib('dom'); // at least javascript-static.js needs to be migrated to YUI3
333 $this->string_for_js('confirmation', 'admin');
334 $this->string_for_js('cancel', 'moodle');
335 $this->string_for_js('yes', 'moodle');
337 if ($page->pagelayout === 'frametop') {
338 $this->js_init_call('M.util.init_frametop');
343 * Ensure that the specified JavaScript file is linked to from this page.
345 * NOTE: This function is to be used in rare cases only, please store your JS in module.js file
346 * and use $PAGE->requires->js_init_call() instead.
348 * By default the link is put at the end of the page, since this gives best page-load performance.
350 * Even if a particular script is requested more than once, it will only be linked
351 * to once.
353 * @param string|moodle_url $url The path to the .js file, relative to $CFG->dirroot / $CFG->wwwroot.
354 * For example '/mod/mymod/customscripts.js'; use moodle_url for external scripts
355 * @param bool $inhead initialise in head
357 public function js($url, $inhead = false) {
358 $url = $this->js_fix_url($url);
359 $where = $inhead ? 'head' : 'footer';
360 $this->jsincludes[$where][$url->out()] = $url;
364 * Ensure that the specified YUI2 library file, and all its required dependencies,
365 * are linked to from this page.
367 * By default the link is put at the end of the page, since this gives best page-load
368 * performance. Optional dependencies are not loaded automatically - if you want
369 * them you will need to load them first with other calls to this method.
371 * Even if a particular library is requested more than once (perhaps as a dependency
372 * of other libraries) it will only be linked to once.
374 * The library is leaded as soon as possible, if $OUTPUT->header() not used yet it
375 * is put into the page header, otherwise it is loaded in the page footer.
377 * @param string|array $libname the name of the YUI2 library you require. For example 'autocomplete'.
379 public function yui2_lib($libname) {
380 $libnames = (array)$libname;
381 foreach ($libnames as $lib) {
382 $this->yui2loader->load($lib);
387 * Returns the actual url through which a script is served.
389 * @param moodle_url|string $url full moodle url, or shortened path to script
390 * @return moodle_url
392 protected function js_fix_url($url) {
393 global $CFG;
395 if ($url instanceof moodle_url) {
396 return $url;
397 } else if (strpos($url, '/') === 0) {
398 if (debugging()) {
399 // check file existence only when in debug mode
400 if (!file_exists($CFG->dirroot . strtok($url, '?'))) {
401 throw new coding_exception('Attempt to require a JavaScript file that does not exist.', $url);
404 if (!empty($CFG->cachejs) and !empty($CFG->jsrev) and strpos($url, '/lib/editor/') !== 0 and substr($url, -3) === '.js') {
405 return new moodle_url($CFG->httpswwwroot.'/lib/javascript.php', array('file'=>$url, 'rev'=>$CFG->jsrev));
406 } else {
407 return new moodle_url($CFG->httpswwwroot.$url);
409 } else {
410 throw new coding_exception('Invalid JS url, it has to be shortened url starting with / or moodle_url instance.', $url);
415 * Find out if JS module present and return details.
417 * @param string $component name of component in frankenstyle, ex: core_group, mod_forum
418 * @return array description of module or null if not found
420 protected function find_module($component) {
421 global $CFG;
423 $module = null;
425 if (strpos($component, 'core_') === 0) {
426 // must be some core stuff - list here is not complete, this is just the stuff used from multiple places
427 // so that we do nto have to repeat the definition of these modules over and over again
428 switch($component) {
429 case 'core_filepicker':
430 $module = array('name' => 'core_filepicker',
431 'fullpath' => '/repository/filepicker.js',
432 'requires' => array('base', 'node', 'node-event-simulate', 'json', 'async-queue', 'io-base', 'io-upload-iframe', 'io-form', 'yui2-button', 'yui2-container', 'yui2-layout', 'yui2-menu', 'yui2-treeview', 'yui2-dragdrop', 'yui2-cookie'),
433 'strings' => array(array('add', 'repository'), array('back', 'repository'), array('cancel', 'moodle'), array('close', 'repository'),
434 array('cleancache', 'repository'), array('copying', 'repository'), array('date', 'repository'), array('downloadsucc', 'repository'),
435 array('emptylist', 'repository'), array('error', 'repository'), array('federatedsearch', 'repository'),
436 array('filenotnull', 'repository'), array('getfile', 'repository'), array('help', 'moodle'), array('iconview', 'repository'),
437 array('invalidjson', 'repository'), array('linkexternal', 'repository'), array('listview', 'repository'),
438 array('loading', 'repository'), array('login', 'repository'), array('logout', 'repository'), array('noenter', 'repository'),
439 array('noresult', 'repository'), array('manageurl', 'repository'), array('popup', 'repository'), array('preview', 'repository'),
440 array('refresh', 'repository'), array('save', 'repository'), array('saveas', 'repository'), array('saved', 'repository'),
441 array('saving', 'repository'), array('search', 'repository'), array('searching', 'repository'), array('size', 'repository'),
442 array('submit', 'repository'), array('sync', 'repository'), array('title', 'repository'), array('upload', 'repository'),
443 array('uploading', 'repository'), array('xhtmlerror', 'repository'),
444 array('cancel'), array('chooselicense', 'repository'), array('author', 'repository'),array('next', 'moodle'),
445 array('ok', 'moodle'), array('error', 'moodle'), array('info', 'moodle'), array('norepositoriesavailable', 'repository'), array('norepositoriesexternalavailable', 'repository'),
446 array('nofilesattached', 'repository'), array('filepicker', 'repository'),
447 array('nofilesavailable', 'repository'), array('overwrite', 'repository'),
448 array('renameto', 'repository'), array('fileexists', 'repository'),
449 array('fileexistsdialogheader', 'repository'), array('fileexistsdialog_editor', 'repository'),
450 array('fileexistsdialog_filemanager', 'repository')
452 break;
453 case 'core_comment':
454 $module = array('name' => 'core_comment',
455 'fullpath' => '/comment/comment.js',
456 'requires' => array('base', 'io-base', 'node', 'json', 'yui2-animation', 'overlay'),
457 'strings' => array(array('confirmdeletecomments', 'admin'), array('yes', 'moodle'), array('no', 'moodle'))
459 break;
460 case 'core_role':
461 $module = array('name' => 'core_role',
462 'fullpath' => '/admin/roles/module.js',
463 'requires' => array('node', 'cookie'));
464 break;
465 case 'core_completion':
466 $module = array('name' => 'core_completion',
467 'fullpath' => '/course/completion.js');
468 break;
469 case 'core_dock':
470 $module = array('name' => 'core_dock',
471 'fullpath' => '/blocks/dock.js',
472 'requires' => array('base', 'node', 'event-custom', 'event-mouseenter', 'event-resize'),
473 'strings' => array(array('addtodock', 'block'),array('undockitem', 'block'),array('undockall', 'block'),array('thisdirectionvertical', 'langconfig')));
474 break;
475 case 'core_message':
476 $module = array('name' => 'core_message',
477 'requires' => array('base', 'node', 'event', 'node-event-simulate'),
478 'fullpath' => '/message/module.js');
479 break;
480 case 'core_group':
481 $module = array('name' => 'core_group',
482 'fullpath' => '/group/module.js',
483 'requires' => array('node', 'overlay', 'event-mouseenter'));
484 break;
485 case 'core_question_engine':
486 $module = array('name' => 'core_question_engine',
487 'fullpath' => '/question/qengine.js',
488 'requires' => array('node', 'event'));
489 break;
490 case 'core_rating':
491 $module = array('name' => 'core_rating',
492 'fullpath' => '/rating/module.js',
493 'requires' => array('node', 'event', 'overlay', 'io-base', 'json'));
494 break;
495 case 'core_filetree':
496 $module = array('name' => 'core_filetree',
497 'fullpath' => '/files/module.js',
498 'requires' => array('node', 'event', 'overlay', 'io-base', 'json', 'yui2-treeview'));
499 break;
500 case 'core_dndupload':
501 $module = array('name' => 'core_dndupload',
502 'fullpath' => '/lib/form/dndupload.js',
503 'requires' => array('node', 'event', 'json'),
504 'strings' => array(array('uploadformlimit', 'moodle'), array('droptoupload', 'moodle'), array('maxfilesreached', 'moodle'), array('dndenabled_inbox', 'moodle')));
505 break;
508 } else {
509 if ($dir = get_component_directory($component)) {
510 if (file_exists("$dir/module.js")) {
511 if (strpos($dir, $CFG->dirroot.'/') === 0) {
512 $dir = substr($dir, strlen($CFG->dirroot));
513 $module = array('name'=>$component, 'fullpath'=>"$dir/module.js", 'requires' => array());
519 return $module;
523 * Append YUI3 module to default YUI3 JS loader.
524 * The structure of module array is described at {@link http://developer.yahoo.com/yui/3/yui/}
526 * @param string|array $module name of module (details are autodetected), or full module specification as array
527 * @return void
529 public function js_module($module) {
530 global $CFG;
532 if (empty($module)) {
533 throw new coding_exception('Missing YUI3 module name or full description.');
536 if (is_string($module)) {
537 $module = $this->find_module($module);
540 if (empty($module) or empty($module['name']) or empty($module['fullpath'])) {
541 throw new coding_exception('Missing YUI3 module details.');
544 // Don't load this module if we already have, no need to!
545 if ($this->js_module_loaded($module['name'])) {
546 if (debugging('', DEBUG_DEVELOPER)) {
547 $this->debug_moduleloadstacktraces[$module['name']][] = format_backtrace(debug_backtrace());
549 return;
552 $module['fullpath'] = $this->js_fix_url($module['fullpath'])->out(false);
553 // add all needed strings
554 if (!empty($module['strings'])) {
555 foreach ($module['strings'] as $string) {
556 $identifier = $string[0];
557 $component = isset($string[1]) ? $string[1] : 'moodle';
558 $a = isset($string[2]) ? $string[2] : null;
559 $this->string_for_js($identifier, $component, $a);
562 unset($module['strings']);
564 // Process module requirements and attempt to load each. This allows
565 // moodle modules to require each other.
566 if (!empty($module['requires'])){
567 foreach ($module['requires'] as $requirement) {
568 $rmodule = $this->find_module($requirement);
569 if (is_array($rmodule)) {
570 $this->js_module($rmodule);
575 if ($this->headdone) {
576 $this->extramodules[$module['name']] = $module;
577 } else {
578 $this->M_yui_loader->modules[$module['name']] = $module;
580 if (debugging('', DEBUG_DEVELOPER)) {
581 if (!array_key_exists($module['name'], $this->debug_moduleloadstacktraces)) {
582 $this->debug_moduleloadstacktraces[$module['name']] = array();
584 $this->debug_moduleloadstacktraces[$module['name']][] = format_backtrace(debug_backtrace());
589 * Returns true if the module has already been loaded.
591 * @param string|array $module
592 * @return bool True if the module has already been loaded
594 protected function js_module_loaded($module) {
595 if (is_string($module)) {
596 $modulename = $module;
597 } else {
598 $modulename = $module['name'];
600 return array_key_exists($modulename, $this->M_yui_loader->modules) ||
601 array_key_exists($modulename, $this->extramodules);
605 * Returns the stacktraces from loading js modules.
606 * @return array
608 public function get_loaded_modules() {
609 return $this->debug_moduleloadstacktraces;
613 * Ensure that the specified CSS file is linked to from this page.
615 * Because stylesheet links must go in the <head> part of the HTML, you must call
616 * this function before {@link get_head_code()} is called. That normally means before
617 * the call to print_header. If you call it when it is too late, an exception
618 * will be thrown.
620 * Even if a particular style sheet is requested more than once, it will only
621 * be linked to once.
623 * Please note use of this feature is strongly discouraged,
624 * it is suitable only for places where CSS is submitted directly by teachers.
625 * (Students must not be allowed to submit any external CSS because it may
626 * contain embedded javascript!). Example of correct use is mod/data.
628 * @param string $stylesheet The path to the .css file, relative to $CFG->wwwroot.
629 * For example:
630 * $PAGE->requires->css('mod/data/css.php?d='.$data->id);
632 public function css($stylesheet) {
633 global $CFG;
635 if ($this->headdone) {
636 throw new coding_exception('Cannot require a CSS file after &lt;head> has been printed.', $stylesheet);
639 if ($stylesheet instanceof moodle_url) {
640 // ok
641 } else if (strpos($stylesheet, '/') === 0) {
642 $stylesheet = new moodle_url($CFG->httpswwwroot.$stylesheet);
643 } else {
644 throw new coding_exception('Invalid stylesheet parameter.', $stylesheet);
647 $this->cssurls[$stylesheet->out()] = $stylesheet; // overrides
651 * Add theme stylkesheet to page - do not use from plugin code,
652 * this should be called only from the core renderer!
654 * @param moodle_url $stylesheet
655 * @return void
657 public function css_theme(moodle_url $stylesheet) {
658 $this->cssthemeurls[] = $stylesheet;
662 * Ensure that a skip link to a given target is printed at the top of the <body>.
664 * You must call this function before {@link get_top_of_body_code()}, (if not, an exception
665 * will be thrown). That normally means you must call this before the call to print_header.
667 * If you ask for a particular skip link to be printed, it is then your responsibility
668 * to ensure that the appropriate <a name="..."> tag is printed in the body of the
669 * page, so that the skip link goes somewhere.
671 * Even if a particular skip link is requested more than once, only one copy of it will be output.
673 * @param $target the name of anchor this link should go to. For example 'maincontent'.
674 * @param $linktext The text to use for the skip link. Normally get_string('skipto', 'access', ...);
676 public function skip_link_to($target, $linktext) {
677 if ($this->topofbodydone) {
678 debugging('Page header already printed, can not add skip links any more, code needs to be fixed.');
679 return;
681 $this->skiplinks[$target] = $linktext;
685 * !!!DEPRECATED!!! please use js_init_call() if possible
686 * Ensure that the specified JavaScript function is called from an inline script
687 * somewhere on this page.
689 * By default the call will be put in a script tag at the
690 * end of the page after initialising Y instance, since this gives best page-load
691 * performance and allows you to use YUI3 library.
693 * If you request that a particular function is called several times, then
694 * that is what will happen (unlike linking to a CSS or JS file, where only
695 * one link will be output).
697 * The main benefit of the method is the automatic encoding of all function parameters.
699 * @param string $function the name of the JavaScritp function to call. Can
700 * be a compound name like 'Y.Event.purgeElement'. Can also be
701 * used to create and object by using a 'function name' like 'new user_selector'.
702 * @param array $arguments and array of arguments to be passed to the function.
703 * When generating the function call, this will be escaped using json_encode,
704 * so passing objects and arrays should work.
705 * @param bool $ondomready If tru the function is only called when the dom is
706 * ready for manipulation.
707 * @param int $delay The delay before the function is called.
709 public function js_function_call($function, array $arguments = null, $ondomready = false, $delay = 0) {
710 $where = $ondomready ? 'ondomready' : 'normal';
711 $this->jscalls[$where][] = array($function, $arguments, $delay);
715 * Adds a call to make use of a YUI gallery module. DEPRECATED DO NOT USE!!!
717 * @deprecated DO NOT USE
719 * @param string|array $modules One or more gallery modules to require
720 * @param string $version
721 * @param string $function
722 * @param array $arguments
723 * @param bool $ondomready
725 public function js_gallery_module($modules, $version, $function, array $arguments = null, $ondomready = false) {
726 global $CFG;
727 debugging('This function will be removed before 2.0 is released please change it from js_gallery_module to yui_module', DEBUG_DEVELOPER);
728 $this->yui_module($modules, $function, $arguments, $version, $ondomready);
732 * Creates a JavaScript function call that requires one or more modules to be loaded
734 * This function can be used to include all of the standard YUI module types within JavaScript:
735 * - YUI3 modules [node, event, io]
736 * - YUI2 modules [yui2-*]
737 * - Moodle modules [moodle-*]
738 * - Gallery modules [gallery-*]
740 * @param array|string $modules One or more modules
741 * @param string $function The function to call once modules have been loaded
742 * @param array $arguments An array of arguments to pass to the function
743 * @param string $galleryversion The gallery version to use
744 * @param bool $ondomready
746 public function yui_module($modules, $function, array $arguments = null, $galleryversion = '2010.04.08-12-35', $ondomready = false) {
747 global $CFG;
749 if (!is_array($modules)) {
750 $modules = array($modules);
752 if (empty($CFG->useexternalyui) || true) {
753 // We need to set the M.yui.galleryversion to the correct version
754 $jscode = 'M.yui.galleryversion='.json_encode($galleryversion).';';
755 } else {
756 // Set Y's config.gallery to the version
757 $jscode = 'Y.config.gallery='.json_encode($galleryversion).';';
759 $jscode .= 'Y.use('.join(',', array_map('json_encode', convert_to_array($modules))).',function() {'.js_writer::function_call($function, $arguments).'});';
760 if ($ondomready) {
761 $jscode = "Y.on('domready', function() { $jscode });";
763 $this->jsinitcode[] = $jscode;
767 * Ensure that the specified JavaScript function is called from an inline script
768 * from page footer.
770 * @param string $function the name of the JavaScritp function to with init code,
771 * usually something like 'M.mod_mymodule.init'
772 * @param array $extraarguments and array of arguments to be passed to the function.
773 * The first argument is always the YUI3 Y instance with all required dependencies
774 * already loaded.
775 * @param bool $ondomready wait for dom ready (helps with some IE problems when modifying DOM)
776 * @param array $module JS module specification array
778 public function js_init_call($function, array $extraarguments = null, $ondomready = false, array $module = null) {
779 $jscode = js_writer::function_call_with_Y($function, $extraarguments);
780 if (!$module) {
781 // detect module automatically
782 if (preg_match('/M\.([a-z0-9]+_[^\.]+)/', $function, $matches)) {
783 $module = $this->find_module($matches[1]);
787 $this->js_init_code($jscode, $ondomready, $module);
791 * Add short static javascript code fragment to page footer.
792 * This is intended primarily for loading of js modules and initialising page layout.
793 * Ideally the JS code fragment should be stored in plugin renderer so that themes
794 * may override it.
795 * @param string $jscode
796 * @param bool $ondomready wait for dom ready (helps with some IE problems when modifying DOM)
797 * @param array $module JS module specification array
799 public function js_init_code($jscode, $ondomready = false, array $module = null) {
800 $jscode = trim($jscode, " ;\n"). ';';
802 if ($module) {
803 $this->js_module($module);
804 $modulename = $module['name'];
805 $jscode = "Y.use('$modulename', function(Y) { $jscode });";
808 if ($ondomready) {
809 $jscode = "Y.on('domready', function() { $jscode });";
812 $this->jsinitcode[] = $jscode;
816 * Make a language string available to JavaScript.
818 * All the strings will be available in a M.str object in the global namespace.
819 * So, for example, after a call to $PAGE->requires->string_for_js('course', 'moodle');
820 * then the JavaScript variable M.str.moodle.course will be 'Course', or the
821 * equivalent in the current language.
823 * The arguments to this function are just like the arguments to get_string
824 * except that $component is not optional, and there are some aspects to consider
825 * when the string contains {$a} placeholder.
827 * If the string does not contain any {$a} placeholder, you can simply use
828 * M.str.component.identifier to obtain it. If you prefer, you can call
829 * M.util.get_string(identifier, component) to get the same result.
831 * If you need to use {$a} placeholders, there are two options. Either the
832 * placeholder should be substituted in PHP on server side or it should
833 * be substituted in Javascript at client side.
835 * To substitute the placeholder at server side, just provide the required
836 * value for the placeholder when you require the string. Because each string
837 * is only stored once in the JavaScript (based on $identifier and $module)
838 * you cannot get the same string with two different values of $a. If you try,
839 * an exception will be thrown. Once the placeholder is substituted, you can
840 * use M.str or M.util.get_string() as shown above:
842 * // require the string in PHP and replace the placeholder
843 * $PAGE->requires->string_for_js('fullnamedisplay', 'moodle', $USER);
844 * // use the result of the substitution in Javascript
845 * alert(M.str.moodle.fullnamedisplay);
847 * To substitute the placeholder at client side, use M.util.get_string()
848 * function. It implements the same logic as {@link get_string()}:
850 * // require the string in PHP but keep {$a} as it is
851 * $PAGE->requires->string_for_js('fullnamedisplay', 'moodle');
852 * // provide the values on the fly in Javascript
853 * user = { firstname : 'Harry', lastname : 'Potter' }
854 * alert(M.util.get_string('fullnamedisplay', 'moodle', user);
856 * If you do need the same string expanded with different $a values in PHP
857 * on server side, then the solution is to put them in your own data structure
858 * (e.g. and array) that you pass to JavaScript with {@link data_for_js()}.
860 * @param string $identifier the desired string.
861 * @param string $component the language file to look in.
862 * @param mixed $a any extra data to add into the string (optional).
864 public function string_for_js($identifier, $component, $a = NULL) {
865 $string = get_string($identifier, $component, $a);
866 if (!$component) {
867 throw new coding_exception('The $module parameter is required for page_requirements_manager::string_for_js.');
869 if (isset($this->stringsforjs[$component][$identifier]) && $this->stringsforjs[$component][$identifier] !== $string) {
870 throw new coding_exception("Attempt to re-define already required string '$identifier' " .
871 "from lang file '$component'. Did you already ask for it with a different \$a? {$this->stringsforjs[$component][$identifier]} !== $string");
873 $this->stringsforjs[$component][$identifier] = $string;
877 * Make an array of language strings available for JS
879 * This function calls the above function {@link string_for_js()} for each requested
880 * string in the $identifiers array that is passed to the argument for a single module
881 * passed in $module.
883 * <code>
884 * $PAGE->requires->strings_for_js(array('one', 'two', 'three'), 'mymod', array('a', null, 3));
886 * // The above is identitical to calling
888 * $PAGE->requires->string_for_js('one', 'mymod', 'a');
889 * $PAGE->requires->string_for_js('two', 'mymod');
890 * $PAGE->requires->string_for_js('three', 'mymod', 3);
891 * </code>
893 * @param array $identifiers An array of desired strings
894 * @param string $component The module to load for
895 * @param mixed $a This can either be a single variable that gets passed as extra
896 * information for every string or it can be an array of mixed data where the
897 * key for the data matches that of the identifier it is meant for.
900 public function strings_for_js($identifiers, $component, $a=NULL) {
901 foreach ($identifiers as $key => $identifier) {
902 if (is_array($a) && array_key_exists($key, $a)) {
903 $extra = $a[$key];
904 } else {
905 $extra = $a;
907 $this->string_for_js($identifier, $component, $extra);
912 * !!!!!!DEPRECATED!!!!!! please use js_init_call() for everything now.
914 * Make some data from PHP available to JavaScript code.
916 * For example, if you call
917 * <pre>
918 * $PAGE->requires->data_for_js('mydata', array('name' => 'Moodle'));
919 * </pre>
920 * then in JavsScript mydata.name will be 'Moodle'.
921 * @param string $variable the the name of the JavaScript variable to assign the data to.
922 * Will probably work if you use a compound name like 'mybuttons.button[1]', but this
923 * should be considered an experimental feature.
924 * @param mixed $data The data to pass to JavaScript. This will be escaped using json_encode,
925 * so passing objects and arrays should work.
926 * @param bool $inhead initialise in head
927 * @return void
929 public function data_for_js($variable, $data, $inhead=false) {
930 $where = $inhead ? 'head' : 'footer';
931 $this->jsinitvariables[$where][] = array($variable, $data);
935 * Creates a YUI event handler.
937 * @param mixed $selector standard YUI selector for elemnts, may be array or string, element id is in the form "#idvalue"
938 * @param string $event A valid DOM event (click, mousedown, change etc.)
939 * @param string $function The name of the function to call
940 * @param array $arguments An optional array of argument parameters to pass to the function
942 public function event_handler($selector, $event, $function, array $arguments = null) {
943 $this->eventhandlers[] = array('selector'=>$selector, 'event'=>$event, 'function'=>$function, 'arguments'=>$arguments);
947 * Returns code needed for registering of event handlers.
948 * @return string JS code
950 protected function get_event_handler_code() {
951 $output = '';
952 foreach ($this->eventhandlers as $h) {
953 $output .= js_writer::event_handler($h['selector'], $h['event'], $h['function'], $h['arguments']);
955 return $output;
959 * Get the inline JavaScript code that need to appear in a particular place.
960 * @param bool $ondomready
961 * @return string
963 protected function get_javascript_code($ondomready) {
964 $where = $ondomready ? 'ondomready' : 'normal';
965 $output = '';
966 if ($this->jscalls[$where]) {
967 foreach ($this->jscalls[$where] as $data) {
968 $output .= js_writer::function_call($data[0], $data[1], $data[2]);
970 if (!empty($ondomready)) {
971 $output = " Y.on('domready', function() {\n$output\n });";
974 return $output;
978 * Returns js code to be executed when Y is available.
979 * @return string
981 protected function get_javascript_init_code() {
982 if (count($this->jsinitcode)) {
983 return implode("\n", $this->jsinitcode) . "\n";
985 return '';
989 * Returns basic YUI3 JS loading code.
990 * YUI3 is using autoloading of both CSS and JS code.
992 * Major benefit of this compared to standard js/csss loader is much improved
993 * caching, better browser cache utilisation, much fewer http requests.
995 * @return string
997 protected function get_yui3lib_headcode() {
998 global $CFG;
1000 $code = '';
1002 if ($this->yui3loader->combine) {
1003 $code .= '<link rel="stylesheet" type="text/css" href="'.$this->yui3loader->comboBase
1004 .$CFG->yui3version.'/build/cssreset/reset-min.css&amp;'
1005 .$CFG->yui3version.'/build/cssfonts/fonts-min.css&amp;'
1006 .$CFG->yui3version.'/build/cssgrids/grids-min.css&amp;'
1007 .$CFG->yui3version.'/build/cssbase/base-min.css" />';
1008 $code .= '<script type="text/javascript" src="'.$this->yui3loader->comboBase.$CFG->yui3version.'/build/yui/yui-min.js"></script>';
1009 } else {
1010 $code .= '<link rel="stylesheet" type="text/css" href="'.$this->yui3loader->base.'cssreset/reset-min.css" />';
1011 $code .= '<link rel="stylesheet" type="text/css" href="'.$this->yui3loader->base.'cssfonts/fonts-min.css" />';
1012 $code .= '<link rel="stylesheet" type="text/css" href="'.$this->yui3loader->base.'cssgrids/grids-min.css" />';
1013 $code .= '<link rel="stylesheet" type="text/css" href="'.$this->yui3loader->base.'cssbase/base-min.css" />';
1014 $code .= '<script type="text/javascript" src="'.$this->yui3loader->base.'yui/yui-min.js"></script>';
1018 if ($this->yui3loader->filter === YUI_RAW) {
1019 $code = str_replace('-min.css', '.css', $code);
1020 $code = str_replace('-min.js', '.js', $code);
1021 } else if ($this->yui3loader->filter === YUI_DEBUG) {
1022 $code = str_replace('-min.css', '.css', $code);
1023 $code = str_replace('-min.js', '-debug.js', $code);
1026 return $code;
1030 * Returns basic YUI2 JS loading code.
1031 * It can be called manually at any time.
1032 * If called manually the result needs to be output using echo().
1034 * Major benefit of this compared to standard js loader is much improved
1035 * caching, better browser cache utilisation, much fewer http requests.
1037 * All YUI2 CSS is loaded automatically.
1039 * @return string JS embedding code
1041 public function get_yui2lib_code() {
1042 global $CFG;
1044 if ($this->headdone) {
1045 $code = $this->yui2loader->script();
1046 } else {
1047 $code = $this->yui2loader->script();
1048 if ($this->yui2loader->combine) {
1049 $skinurl = $this->yui2loader->comboBase . $CFG->yui2version . '/build/assets/skins/sam/skin.css';
1050 } else {
1051 $skinurl = $this->yui2loader->base . 'assets/skins/sam/skin.css';
1053 // please note this is a temporary hack until we fully migrate to later YUI3 that has all the widgets
1054 $attributes = array('rel'=>'stylesheet', 'type'=>'text/css', 'href'=>$skinurl);
1055 $code .= "\n" . html_writer::empty_tag('link', $attributes) . "\n";
1057 $code = str_replace('&amp;', '&', $code);
1058 $code = str_replace('&', '&amp;', $code);
1059 return $code;
1063 * Returns html tags needed for inclusion of theme CSS
1065 * @return string
1067 protected function get_css_code() {
1068 // First of all the theme CSS, then any custom CSS
1069 // Please note custom CSS is strongly discouraged,
1070 // because it can not be overridden by themes!
1071 // It is suitable only for things like mod/data which accepts CSS from teachers.
1072 $attributes = array('rel'=>'stylesheet', 'type'=>'text/css');
1074 // This line of code may look funny but it is currently required in order
1075 // to avoid MASSIVE display issues in Internet Explorer.
1076 // As of IE8 + YUI3.1.1 the reference stylesheet (firstthemesheet) gets
1077 // ignored whenever another resource is added until such time as a redraw
1078 // is forced, usually by moving the mouse over the affected element.
1079 $code = html_writer::tag('script', '/** Required in order to fix style inclusion problems in IE with YUI **/', array('id'=>'firstthemesheet', 'type'=>'text/css'));
1081 $urls = $this->cssthemeurls + $this->cssurls;
1082 foreach ($urls as $url) {
1083 $attributes['href'] = $url;
1084 $code .= html_writer::empty_tag('link', $attributes) . "\n";
1085 // this id is needed in first sheet only so that theme may override YUI sheets laoded on the fly
1086 unset($attributes['id']);
1089 return $code;
1093 * Adds extra modules specified after printing of page header
1095 * @return string
1097 protected function get_extra_modules_code() {
1098 if (empty($this->extramodules)) {
1099 return '';
1101 return html_writer::script(js_writer::function_call('M.yui.add_module', array($this->extramodules)));
1105 * Generate any HTML that needs to go inside the <head> tag.
1107 * Normally, this method is called automatically by the code that prints the
1108 * <head> tag. You should not normally need to call it in your own code.
1110 * @param moodle_page $page
1111 * @param core_renderer $renderer
1112 * @return string the HTML code to to inside the <head> tag.
1114 public function get_head_code(moodle_page $page, core_renderer $renderer) {
1115 global $CFG;
1117 // note: the $page and $output are not stored here because it would
1118 // create circular references in memory which prevents garbage collection
1119 $this->init_requirements_data($page, $renderer);
1121 // yui3 JS and CSS is always loaded first - it is cached in browser
1122 $output = $this->get_yui3lib_headcode();
1124 // BC: load basic YUI2 for now, all yui2 things should be loaded via Y.use('yui2-oldmodulename')
1125 $output .= $this->get_yui2lib_code();
1127 // now theme CSS + custom CSS in this specific order
1128 $output .= $this->get_css_code();
1130 // set up global YUI3 loader object - this should contain all code needed by plugins
1131 // note: in JavaScript just use "YUI(M.yui.loader).use('overlay', function(Y) { .... });"
1132 // this needs to be done before including any other script
1133 $js = "var M = {}; M.yui = {}; var moodleConfigFn = function(me) {var p = me.path, b = me.name.replace(/^moodle-/,'').split('-', 3), n = b.pop();if (/(skin|core)/.test(n)) {n = b.pop();me.type = 'css';};me.path = b.join('-')+'/'+n+'/'+n+'.'+me.type;}; var galleryConfigFn = function(me) {var p = me.path,v=M.yui.galleryversion,f;if(/-(skin|core)/.test(me.name)) {me.type = 'css';p = p.replace(/-(skin|core)/, '').replace(/\.js/, '.css').split('/'), f = p.pop().replace(/(\-(min|debug))/, '');if (/-skin/.test(me.name)) {p.splice(p.length,0,v,'assets','skins','sam', f);} else {p.splice(p.length,0,v,'assets', f);};} else {p = p.split('/'), f = p.pop();p.splice(p.length,0,v, f);};me.path = p.join('/');};\n";
1134 $js .= js_writer::set_variable('M.yui.loader', $this->M_yui_loader, false) . "\n";
1135 $js .= js_writer::set_variable('M.cfg', $this->M_cfg, false);
1136 $js = str_replace('"@GALLERYCONFIGFN@"', 'galleryConfigFn', $js);
1137 $js = str_replace('"@MOODLECONFIGFN@"', 'moodleConfigFn', $js);
1139 $output .= html_writer::script($js);
1141 // link our main JS file, all core stuff should be there
1142 $output .= html_writer::script('', $this->js_fix_url('/lib/javascript-static.js'));
1144 // add variables
1145 if ($this->jsinitvariables['head']) {
1146 $js = '';
1147 foreach ($this->jsinitvariables['head'] as $data) {
1148 list($var, $value) = $data;
1149 $js .= js_writer::set_variable($var, $value, true);
1151 $output .= html_writer::script($js);
1154 // all the other linked things from HEAD - there should be as few as possible
1155 if ($this->jsincludes['head']) {
1156 foreach ($this->jsincludes['head'] as $url) {
1157 $output .= html_writer::script('', $url);
1161 // mark head sending done, it is not possible to anything there
1162 $this->headdone = true;
1164 return $output;
1168 * Generate any HTML that needs to go at the start of the <body> tag.
1170 * Normally, this method is called automatically by the code that prints the
1171 * <head> tag. You should not normally need to call it in your own code.
1173 * @return string the HTML code to go at the start of the <body> tag.
1175 public function get_top_of_body_code() {
1176 // first the skip links
1177 $links = '';
1178 $attributes = array('class'=>'skip');
1179 foreach ($this->skiplinks as $url => $text) {
1180 $attributes['href'] = '#' . $url;
1181 $links .= html_writer::tag('a', $text, $attributes);
1183 $output = html_writer::tag('div', $links, array('class'=>'skiplinks')) . "\n";
1185 // then the clever trick for hiding of things not needed when JS works
1186 $output .= html_writer::script("document.body.className += ' jsenabled';") . "\n";
1187 $this->topofbodydone = true;
1188 return $output;
1192 * Generate any HTML that needs to go at the end of the page.
1194 * Normally, this method is called automatically by the code that prints the
1195 * page footer. You should not normally need to call it in your own code.
1197 * @return string the HTML code to to at the end of the page.
1199 public function get_end_code() {
1200 global $CFG;
1201 // add other requested modules
1202 $output = $this->get_extra_modules_code();
1204 // add missing YUI2 YUI - to be removed once we convert everything to YUI3!
1205 $output .= $this->get_yui2lib_code();
1207 // all the other linked scripts - there should be as few as possible
1208 if ($this->jsincludes['footer']) {
1209 foreach ($this->jsincludes['footer'] as $url) {
1210 $output .= html_writer::script('', $url);
1214 // add all needed strings
1215 if (!empty($this->stringsforjs)) {
1216 $output .= html_writer::script(js_writer::set_variable('M.str', $this->stringsforjs));
1219 // add variables
1220 if ($this->jsinitvariables['footer']) {
1221 $js = '';
1222 foreach ($this->jsinitvariables['footer'] as $data) {
1223 list($var, $value) = $data;
1224 $js .= js_writer::set_variable($var, $value, true);
1226 $output .= html_writer::script($js);
1229 $inyuijs = $this->get_javascript_code(false);
1230 $ondomreadyjs = $this->get_javascript_code(true);
1231 $jsinit = $this->get_javascript_init_code();
1232 $handlersjs = $this->get_event_handler_code();
1234 // there is no global Y, make sure it is available in your scope
1235 $js = "YUI(M.yui.loader).use('node', function(Y) {\n{$inyuijs}{$ondomreadyjs}{$jsinit}{$handlersjs}\n});";
1237 $output .= html_writer::script($js);
1239 return $output;
1243 * Have we already output the code in the <head> tag?
1245 * @return bool
1247 public function is_head_done() {
1248 return $this->headdone;
1252 * Have we already output the code at the start of the <body> tag?
1254 * @return bool
1256 public function is_top_of_body_done() {
1257 return $this->topofbodydone;
1262 * Invalidate all server and client side JS caches.
1264 function js_reset_all_caches() {
1265 global $CFG;
1266 require_once("$CFG->libdir/filelib.php");
1268 set_config('jsrev', empty($CFG->jsrev) ? 1 : $CFG->jsrev+1);
1269 fulldelete("$CFG->cachedir/js");