"MDL-19118, improve comments management"
[moodle.git] / lib / outputrequirementslib.php
CommitLineData
0bb38e8c
PS
1<?php
2
3// This file is part of Moodle - http://moodle.org/
4//
5// Moodle is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// Moodle is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17
18
19/**
20 * Library functions to facilitate the use of JavaScript in Moodle.
21 *
22 * @package moodlecore
23 * @copyright 2009 Tim Hunt, 2010 Petr Skoda
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 */
26
27// note: you can find history of this file in lib/ajax/ajaxlib.php
28
29/**
30 * This class tracks all the things that are needed by the current page.
31 *
32 * Normally, the only instance of this class you will need to work with is the
33 * one accessible via $PAGE->requires.
34 *
35 * Typical useage would be
36 * <pre>
37 * $PAGE->requires->init_js_call('M.mod_forum.init_view');
38 * </pre>
39 *
40 * It also supports obsoleted coding style withouth YUI3 modules.
41 * <pre>
42 * $PAGE->requires->css('/mod/mymod/userstyles.php?id='.$id); // not overriddable via themes!
43 * $PAGE->requires->js('/mod/mymod/script.js');
44 * $PAGE->requires->js('/mod/mymod/small_but_urgent.js', true);
45 * $PAGE->requires->js_function_call('init_mymod', array($data), true);
46 * </pre>
47 *
48 * There are some natural restrictions on some methods. For example, {@link css()}
49 * can only be called before the <head> tag is output. See the comments on the
50 * individual methods for details.
51 *
52 * @copyright 2009 Tim Hunt, 2010 Petr Skoda
53 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
54 * @since Moodle 2.0
55 */
56class page_requirements_manager {
57 /** List of string available from JS */
58 protected $stringsforjs = array();
59 /** List of JS variables to be initialised */
60 protected $jsinitvariables = array('head'=>array(), 'footer'=>array());
61 /** Included JS scripts */
62 protected $jsincludes = array('head'=>array(), 'footer'=>array());
63 /** List of needed function calls */
64 protected $jscalls = array('normal'=>array(), 'ondomready'=>array());
65 /**
66 * List of skip links, those are needed for accessibility reasons
67 * @var array
68 */
69 protected $skiplinks = array();
70 /**
71 * Javascript code used for initialisation of page, it shoudl be relatively small
72 * @var array
73 */
74 protected $jsinitcode = array();
75 /**
76 * Theme sheets, initialised only from core_renderer
77 * @var array of moodle_url
78 */
79 protected $cssthemeurls = array();
80 /**
81 * List of custom theme sheets, these are strongly discouraged!
82 * Useful mostly only for CSS submitted by teachers that is not part of the theme.
83 * @var array of moodle_url
84 */
85 protected $cssurls = array();
86 /**
87 * List of requested event handlers
88 * @var array
89 */
90 protected $eventhandlers = array();
91 /**
92 * Extra modules
93 * @var array
94 */
95 protected $extramodules = array();
96 /** Flag indicated head stuff already printed */
97 protected $headdone = false;
98 /** Flag indicating top of body already printed */
99 protected $topofbodydone = false;
100
101 /** YUI PHPLoader instance responsible for YUI2 loading from PHP only */
102 protected $yui2loader;
103 /** YUI PHPLoader instance responsible for YUI3 loading from PHP only */
104 protected $yui3loader;
105 /** YUI PHPLoader instance responsible for YUI3 loading from javascript */
106 protected $M_yui_loader;
107 /** some config vars exposed in JS, please no secret stuff there */
108 protected $M_cfg;
109
110 /**
111 * Page requirements constructor.
112 */
113 public function __construct() {
114 global $CFG;
115
116 require_once("$CFG->libdir/yui/phploader/phploader/loader.php");
117
118 $this->yui3loader = new YAHOO_util_Loader($CFG->yui3version);
119 $this->yui2loader = new YAHOO_util_Loader($CFG->yui2version);
120
121 // set up some loader options
122 if (debugging('', DEBUG_DEVELOPER)) {
123 $this->yui3loader->filter = YUI_DEBUG; // alternatively we could use just YUI_RAW here
124 $this->yui2loader->filter = YUI_DEBUG; // alternatively we could use just YUI_RAW here
125 } else {
126 $this->yui3loader->filter = null;
127 $this->yui2loader->filter = null;
128 }
129 if (!empty($CFG->useexternalyui)) {
130 $this->yui3loader->base = 'http://yui.yahooapis.com/' . $CFG->yui3version . '/build/';
131 $this->yui2loader->base = 'http://yui.yahooapis.com/' . $CFG->yui2version . '/build/';
132 $this->yui3loader->comboBase = 'http://yui.yahooapis.com/combo?';
133 $this->yui2loader->comboBase = 'http://yui.yahooapis.com/combo?';
134 } else {
135 $this->yui3loader->base = $CFG->httpswwwroot . '/lib/yui/'. $CFG->yui3version . '/build/';
136 $this->yui2loader->base = $CFG->httpswwwroot . '/lib/yui/'. $CFG->yui2version . '/build/';
137 $this->yui3loader->comboBase = $CFG->httpswwwroot . '/theme/yui_combo.php?';
138 $this->yui2loader->comboBase = $CFG->httpswwwroot . '/theme/yui_combo.php?';
139 }
140
141 // enable combo loader? this significantly helps with caching and performance!
142 $this->yui3loader->combine = !empty($CFG->yuicomboloading);
143 $this->yui2loader->combine = !empty($CFG->yuicomboloading);
144
145 // set up JS YUI loader helper object
146 $this->M_yui_loader = new stdClass();
147 $this->M_yui_loader->base = $this->yui3loader->base;
148 $this->M_yui_loader->comboBase = $this->yui3loader->comboBase;
149 $this->M_yui_loader->combine = $this->yui3loader->combine;
150 $this->M_yui_loader->filter = ($this->yui3loader->filter == YUI_DEBUG) ? 'debug' : '';
151 $this->M_yui_loader->insertBefore = 'firstthemesheet';
152 $this->M_yui_loader->modules = array();
153 $this->add_yui2_modules(); // adds loading info for all YUI2 modules
154 $this->js_module($this->find_module('core_filepicker'));
155 $this->js_module($this->find_module('core_dock'));
156
157 // YUI3 init code
158 $libs = array('cssreset', 'cssbase', 'cssfonts', 'cssgrids', 'node', 'loader'); // full CSS reset + basic libs
159 foreach ($libs as $lib) {
160 $this->yui3loader->load($lib);
161 }
162 }
163
164 /**
165 * This method adds yui2 modules into the yui3 JS loader-
166 * @return void
167 */
168 protected function add_yui2_modules() {
169 //note: this function is definitely not perfect, because
170 // it adds tons of markup into each page, but it can be
171 // abstracted into separate JS file with proper headers
172 global $CFG;
173
174 $GLOBALS['yui_current'] = array();
175 require($CFG->libdir.'/yui/phploader/lib/meta/config_'.$CFG->yui2version.'.php');
176 $info = $GLOBALS['yui_current'];
177 unset($GLOBALS['yui_current']);
178
179 if (empty($CFG->yuicomboloading)) {
180 $urlbase = $this->yui2loader->base;
181 } else {
182 $urlbase = $this->yui2loader->comboBase.$CFG->yui2version.'/build/';
183 }
184
185 $modules = array();
186 $ignored = array(); // list of CSS modules that are not needed
187 foreach ($info['moduleInfo'] as $name => $module) {
188 if ($module['type'] === 'css') {
189 $ignored[$name] = true;
190 } else {
191 $modules['yui2-'.$name] = $module;
192 }
193 }
194 foreach ($modules as $name=>$module) {
195 $module['fullpath'] = $urlbase.$module['path']; // fix path to point to correct location
196 unset($module['path']);
0139ec3f 197 unset($module['skinnable']); // we load all YUI2 css automatically, this prevents weird missing css loader problems
0bb38e8c
PS
198 foreach(array('requires', 'optional', 'supersedes') as $fixme) {
199 if (!empty($module[$fixme])) {
200 $fixed = false;
201 foreach ($module[$fixme] as $key=>$dep) {
202 if (isset($ignored[$dep])) {
203 unset($module[$fixme][$key]);
204 $fixed = true;
205 } else {
206 $module[$fixme][$key] = 'yui2-'.$dep;
207 }
208 }
209 if ($fixed) {
210 $module[$fixme] = array_merge($module[$fixme]); // fix keys
211 }
212 }
213 }
214 $this->M_yui_loader->modules[$name] = $module;
215 }
216 }
217
218 /**
219 * Initialise with the bits of JavaScript that every Moodle page should have.
220 *
221 * @param moodle_page $page
222 * @param core_renderer $output
223 */
224 protected function init_requirements_data(moodle_page $page, core_renderer $renderer) {
225 global $CFG;
226
227 // JavaScript should always work with $CFG->httpswwwroot rather than $CFG->wwwroot.
228 // Otherwise, in some situations, users will get warnings about insecure content
4aea3cc7 229 // on secure pages from their web browser.
0bb38e8c
PS
230
231 $this->M_cfg = array(
232 'wwwroot' => $CFG->httpswwwroot, // Yes, really. See above.
233 'sesskey' => sesskey(),
234 'loadingicon' => $renderer->pix_url('i/loading_small', 'moodle')->out(false),
235 'themerev' => theme_get_revision(),
236 'theme' => $page->theme->name,
237 );
238 if (debugging('', DEBUG_DEVELOPER)) {
239 $this->M_cfg['developerdebug'] = true;
240 $this->yui2_lib('logger');
241 }
242
243 // accessibility stuff
244 $this->skip_link_to('maincontent', get_string('tocontent', 'access'));
245
246 // to be removed soon
247 $this->yui2_lib('dom'); // at least javascript-static.js needs to be migrated to YUI3
248
249 $this->string_for_js('confirmation', 'admin');
250 $this->string_for_js('cancel', 'moodle');
251 $this->string_for_js('yes', 'moodle');
252 $this->js_init_call('M.util.init_help_icons');
4aea3cc7
PS
253
254 if ($page->pagelayout === 'frametop') {
255 $this->js_init_call('M.util.init_frametop');
256 }
0bb38e8c
PS
257 }
258
259 /**
260 * Ensure that the specified JavaScript file is linked to from this page.
261 *
262 * NOTE: This function is to be used in rare cases only, please store your JS in module.js file
263 * and use $PAGE->requires->js_init_call() instead.
264 *
265 * By default the link is put at the end of the page, since this gives best page-load performance.
266 *
267 * Even if a particular script is requested more than once, it will only be linked
268 * to once.
269 *
270 * @param string|moodle_url $url The path to the .js file, relative to $CFG->dirroot / $CFG->wwwroot.
271 * For example '/mod/mymod/customscripts.js'; use moodle_url for external scripts
272 * @param bool $inhead initialise in head
273 * @return void
274 */
275 public function js($url, $inhead=false) {
276 $url = $this->js_fix_url($url);
277 $where = $inhead ? 'head' : 'footer';
278 $this->jsincludes[$where][$url->out()] = $url;
279 }
280
281 /**
282 * Ensure that the specified YUI2 library file, and all its required dependancies,
283 * are linked to from this page.
284 *
285 * By default the link is put at the end of the page, since this gives best page-load
286 * performance. Optional dependencies are not loaded automatically - if you want
287 * them you will need to load them first with other calls to this method.
288 *
289 * Even if a particular library is requested more than once (perhaps as a dependancy
290 * of other libraries) it will only be linked to once.
291 *
292 * The library is leaded as soon as possible, if $OUTPUT->header() not used yet it
293 * is put into the page header, otherwise it is loaded in the page footer.
294 *
295 * @param string|array $libname the name of the YUI2 library you require. For example 'autocomplete'.
296 * @return void
297 */
298 public function yui2_lib($libname) {
299 $libnames = (array)$libname;
300 foreach ($libnames as $lib) {
301 $this->yui2loader->load($lib);
302 }
303 }
304
305 /**
306 * Returns the actual url through which a script is served.
307 * @param moodle_url|string $url full moodle url, or shortened path to script
308 * @return moodle_url
309 */
310 protected function js_fix_url($url) {
311 global $CFG;
312
313 if ($url instanceof moodle_url) {
314 return $url;
315 } else if (strpos($url, '/') === 0) {
316 if (debugging()) {
317 // check file existence only when in debug mode
318 if (!file_exists($CFG->dirroot . strtok($url, '?'))) {
319 throw new coding_exception('Attept to require a JavaScript file that does not exist.', $url);
320 }
321 }
4c1f3942 322 if (!empty($CFG->cachejs) and !empty($CFG->jsrev) and strpos($url, '/lib/editor/') != 0) {
0139ec3f
PS
323 return new moodle_url($CFG->httpswwwroot.'/lib/javascript.php', array('file'=>$url, 'rev'=>$CFG->jsrev));
324 } else {
325 return new moodle_url($CFG->httpswwwroot.$url);
326 }
0bb38e8c
PS
327 } else {
328 throw new coding_exception('Invalid JS url, it has to be shortened url starting with / or moodle_url instance.', $url);
329 }
330 }
331
332 /**
333 * Find out if JS modulke present and return details.
334 * @param string $name name of module, ex: core_group, mod_forum
335 * @return array description of module or null if not found
336 */
337 protected function find_module($name) {
338 global $CFG;
339
340 $module = null;
341
87ad1edc 342
0bb38e8c
PS
343 if (strpos($name, 'core_') === 0) {
344 // must be some core stuff - list here is not complete, this is just the stuff used from multiple places
345 // so that we do nto have to repeat the definition of these modules over and over again
346 switch($name) {
347 case 'core_filepicker':
348 $module = array('name' => 'core_filepicker',
349 'fullpath' => '/repository/filepicker.js',
0dc30338 350 'requires' => array('base', 'node', 'node-event-simulate', 'json', 'async-queue', 'io', 'yui2-button', 'yui2-container', 'yui2-layout', 'yui2-menu', 'yui2-treeview', 'yui2-dragdrop'),
87ad1edc
PS
351 'strings' => array(array('add', 'repository'), array('back', 'repository'), array('cancel', 'moodle'), array('close', 'repository'),
352 array('cleancache', 'repository'), array('copying', 'repository'), array('date', 'repository'), array('downloadsucc', 'repository'),
353 array('emptylist', 'repository'), array('error', 'repository'), array('federatedsearch', 'repository'),
354 array('filenotnull', 'repository'), array('getfile', 'repository'), array('help', 'moodle'), array('iconview', 'repository'),
355 array('invalidjson', 'repository'), array('linkexternal', 'repository'), array('listview', 'repository'),
356 array('loading', 'repository'), array('login', 'repository'), array('logout', 'repository'), array('noenter', 'repository'),
357 array('noresult', 'repository'), array('manageurl', 'repository'), array('popup', 'repository'), array('preview', 'repository'),
358 array('refresh', 'repository'), array('save', 'repository'), array('saveas', 'repository'), array('saved', 'repository'),
359 array('saving', 'repository'), array('search', 'repository'), array('searching', 'repository'), array('size', 'repository'),
360 array('submit', 'repository'), array('sync', 'repository'), array('title', 'repository'), array('upload', 'repository'),
361 array('uploading', 'repository'), array('xhtmlerror', 'repository'),
1dce6261 362 array('xhtml', 'quiz'), array('cancel'), array('chooselicense', 'repository'), array('author', 'repository')));
0bb38e8c
PS
363 break;
364 case 'core_comment':
365 $module = array('name' => 'core_comment',
366 'fullpath' => '/comment/comment.js',
34e20eb4
DC
367 'requires' => array('base', 'io', 'node', 'json', 'yui2-animation'),
368 'strings' => array(array('confirmdeletecomments', 'admin'))
369 );
0bb38e8c
PS
370 break;
371 case 'core_role':
372 $module = array('name' => 'core_role',
373 'fullpath' => '/admin/roles/module.js');
374 break;
375 case 'core_completion':
376 $module = array('name' => 'core_completion',
377 'fullpath' => '/course/completion.js');
378 break;
379 case 'core_dock':
380 $module = array('name' => 'core_dock',
381 'fullpath' => '/blocks/dock.js',
d2e68385 382 'requires' => array('base', 'cookie', 'dom', 'io', 'node', 'event-custom', 'event-mouseenter', 'yui2-container'));
0bb38e8c
PS
383 break;
384 case 'core_calendar':
385 $module = array('name' => 'core_calendar',
386 'fullpath' => '/calendar/calendar.js',
0139ec3f 387 'requires' => array('dom', 'event', 'node', 'yui2-container', 'event-mouseenter'));
0bb38e8c
PS
388 break;
389 case 'core_message':
390 $module = array('name' => 'core_message',
391 'fullpath' => '/message/module.js');
392 break;
393 case 'core_flashdetect':
394 $module = array('name' => 'core_flashdetect',
395 'fullpath' => '/lib/flashdetect/flashdetect.js',
396 'requires' => array('io'));
397 break;
c2489597
SH
398 case 'core_group':
399 $module = array('name' => 'core_group',
400 'fullpath' => '/group/module.js',
401 'requires' => array('node', 'overlay', 'event-mouseenter'));
402 break;
a09aeee4
AD
403 case 'core_ratings':
404 $module = array('name' => 'core_ratings',
405 'fullpath' => '/rating/module.js',
406 'requires' => array('node', 'event', 'overlay', 'io', 'json'));
407 break;
0bb38e8c
PS
408 }
409
410 } else {
411 if ($dir = get_component_directory($name, false)) {
412 if (file_exists("$CFG->dirroot/$dir/module.js")) {
413 $module = array('name'=>$name, 'fullpath'=>"/$dir/module.js", 'requires' => array());
414 }
415 }
416 }
417
418 return $module;
419 }
420
421 /**
422 * Append YUI3 module to default YUI3 JS loader.
423 * The structure of module array is described at http://developer.yahoo.com/yui/3/yui/:
424 * @param string|array $module name of module (details are autodetected), or full module specification as array
425 * @return void
426 */
427 public function js_module($module) {
428 global $CFG;
429
430 if (empty($module)) {
431 throw new coding_exception('Missing YUI3 module name or full description.');
432 }
433
434 if (is_string($module)) {
435 $module = $this->find_module($module);
436 }
437
438 if (empty($module) or empty($module['name']) or empty($module['fullpath'])) {
439 throw new coding_exception('Missing YUI3 module details.');
440 }
441
442 $module['fullpath'] = $this->js_fix_url($module['fullpath'])->out(false);
443
87ad1edc
PS
444 // add all needed strings
445 if (!empty($module['strings'])) {
446 foreach ($module['strings'] as $string) {
447 $identifier = $string[0];
448 $component = isset($string[1]) ? $string[1] : 'moodle';
449 $a = isset($string[2]) ? $string[2] : null;
450 $this->string_for_js($identifier, $component, $a);
451 }
452 }
453 unset($module['strings']);
454
0bb38e8c
PS
455 if ($this->headdone) {
456 $this->extramodules[$module['name']] = $module;
457 } else {
458 $this->M_yui_loader->modules[$module['name']] = $module;
459 }
460 }
461
462 /**
463 * Ensure that the specified CSS file is linked to from this page.
464 *
465 * Because stylesheet links must go in the <head> part of the HTML, you must call
466 * this function before {@link get_head_code()} is called. That normally means before
467 * the call to print_header. If you call it when it is too late, an exception
468 * will be thrown.
469 *
470 * Even if a particular style sheet is requested more than once, it will only
471 * be linked to once.
472 *
473 * Please note sue of this feature is strongly discouraged,
474 * it is suitable only for places where CSS is submitted directly by teachers.
475 * (Students must not be allowed to submit any external CSS because it may
476 * contain embedded javascript!). Example of correct use is mod/data.
477 *
478 * @param string $stylesheet The path to the .css file, relative to $CFG->wwwroot.
479 * For example:
480 * $PAGE->requires->css('mod/data/css.php?d='.$data->id);
481 */
482 public function css($stylesheet) {
483 global $CFG;
484
485 if ($this->headdone) {
486 throw new coding_exception('Cannot require a CSS file after &lt;head> has been printed.', $stylesheet);
487 }
488
489 if ($stylesheet instanceof moodle_url) {
490 // ok
491 } else if (strpos($stylesheet, '/') === 0) {
492 $stylesheet = new moodle_url($CFG->httpswwwroot.$stylesheet);
493 } else {
494 throw new coding_exception('Invalid stylesheet parameter.', $stylesheet);
495 }
496
497 $this->cssurls[$stylesheet->out()] = $stylesheet; // overrides
498 }
499
500 /**
501 * Add theme stylkesheet to page - do not use from plugin code,
502 * this should be called only from the core renderer!
503 * @param moodle_url $stylesheet
504 * @return void
505 */
506 public function css_theme(moodle_url $stylesheet) {
507 $this->cssthemeurls[] = $stylesheet;
508 }
509
510 /**
511 * Ensure that a skip link to a given target is printed at the top of the <body>.
512 *
513 * You must call this function before {@link get_top_of_body_code()}, (if not, an exception
514 * will be thrown). That normally means you must call this before the call to print_header.
515 *
516 * If you ask for a particular skip link to be printed, it is then your responsibility
517 * to ensure that the appropraite <a name="..."> tag is printed in the body of the
518 * page, so that the skip link goes somewhere.
519 *
520 * Even if a particular skip link is requested more than once, only one copy of it will be output.
521 *
522 * @param $target the name of anchor this link should go to. For example 'maincontent'.
523 * @param $linktext The text to use for the skip link. Normally get_string('skipto', 'access', ...);
524 */
525 public function skip_link_to($target, $linktext) {
526 if ($this->topofbodydone) {
527 debugging('Page header already printed, can not add skip links any more, code needs to be fixed.');
528 return;
529 }
530 $this->skiplinks[$target] = $linktext;
531 }
532
533 /**
534 * !!!DEPRECATED!!! please use js_init_call() if possible
535 * Ensure that the specified JavaScript function is called from an inline script
536 * somewhere on this page.
537 *
538 * By default the call will be put in a script tag at the
539 * end of the page after initialising Y instance, since this gives best page-load
540 * performance and allows you to use YUI3 library.
541 *
542 * If you request that a particular function is called several times, then
543 * that is what will happen (unlike linking to a CSS or JS file, where only
544 * one link will be output).
545 *
546 * The main benefit of the mehtod is the automatic encoding of all function parameters.
547 *
548 * @param string $function the name of the JavaScritp function to call. Can
549 * be a compound name like 'Y.Event.purgeElement'. Can also be
550 * used to create and object by using a 'function name' like 'new user_selector'.
551 * @param array $arguments and array of arguments to be passed to the function.
552 * When generating the function call, this will be escaped using json_encode,
553 * so passing objects and arrays should work.
554 * @param bool $ondomready
555 * @param int $delay
556 * @return void
557 */
558 public function js_function_call($function, array $arguments = null, $ondomready = false, $delay = 0) {
559 $where = $ondomready ? 'ondomready' : 'normal';
560 $this->jscalls[$where][] = array($function, $arguments, $delay);
561 }
562
563 /**
564 * Ensure that the specified JavaScript function is called from an inline script
565 * from page footer.
566 *
567 * @param string $function the name of the JavaScritp function to with init code,
568 * usually something like 'M.mod_mymodule.init'
569 * @param array $extraarguments and array of arguments to be passed to the function.
570 * The first argument is always the YUI3 Y instance with all required dependencies
571 * already loaded.
572 * @param bool $ondomready wait for dom ready (helps with some IE problems when modifying DOM)
573 * @param array $module JS module specification array
574 * @return void
575 */
576 public function js_init_call($function, array $extraarguments = null, $ondomready = false, array $module = null) {
577 $jscode = js_writer::function_call_with_Y($function, $extraarguments);
578 if (!$module) {
579 // detect module automatically
580 if (preg_match('/M\.([a-z0-9]+_[^\.]+)/', $function, $matches)) {
581 $module = $this->find_module($matches[1]);
582 }
583 }
584
585 $this->js_init_code($jscode, $ondomready, $module);
586 }
587
588 /**
589 * Add short static javascript code fragment to page footer.
590 * This is intended primarily for loading of js modules and initialising page layout.
591 * Ideally the JS code fragment should be stored in plugin renderer so that themes
592 * may override it.
593 * @param string $jscode
594 * @param bool $ondomready wait for dom ready (helps with some IE problems when modifying DOM)
595 * @param array $module JS module specification array
596 * @return void
597 */
598 public function js_init_code($jscode, $ondomready = false, array $module = null) {
599 $jscode = trim($jscode, " ;\n"). ';';
600
601 if ($module) {
602 $this->js_module($module);
603 $modulename = $module['name'];
604 $jscode = "Y.use('$modulename', function(Y) { $jscode });";
605 }
606
607 if ($ondomready) {
608 $jscode = "Y.on('domready', function() { $jscode });";
609 }
610
611 $this->jsinitcode[] = $jscode;
612 }
613
614 /**
615 * Make a language string available to JavaScript.
616 *
2b728cb5 617 * All the strings will be available in a M.str object in the global namespace.
0bb38e8c 618 * So, for example, after a call to $PAGE->requires->string_for_js('course', 'moodle');
2b728cb5 619 * then the JavaScript variable M.str.moodle.course will be 'Course', or the
0bb38e8c
PS
620 * equivalent in the current language.
621 *
622 * The arguments to this function are just like the arguments to get_string
623 * except that $component is not optional, and there are limitations on how you
624 * use $a. Because each string is only stored once in the JavaScript (based
625 * on $identifier and $module) you cannot get the same string with two different
626 * values of $a. If you try, an exception will be thrown.
627 *
628 * If you do need the same string expanded with different $a values, then
629 * the solution is to put them in your own data structure (e.g. and array)
630 * that you pass to JavaScript with {@link data_for_js()}.
631 *
632 * @param string $identifier the desired string.
633 * @param string $module the language file to look in.
634 * @param mixed $a any extra data to add into the string (optional).
635 */
636 public function string_for_js($identifier, $component, $a = NULL) {
637 $string = get_string($identifier, $component, $a);
638 if (!$component) {
639 throw new coding_exception('The $module parameter is required for page_requirements_manager::string_for_js.');
640 }
641 if (isset($this->stringsforjs[$component][$identifier]) && $this->stringsforjs[$component][$identifier] != $string) {
642 throw new coding_exception("Attempt to re-define already required string '$identifier' " .
643 "from lang file '$component'. Did you already ask for it with a different \$a?");
644 }
645 $this->stringsforjs[$component][$identifier] = $string;
646 }
647
648 /**
649 * Make an array of language strings available for JS
650 *
651 * This function calls the above function {@link string_for_js()} for each requested
652 * string in the $identifiers array that is passed to the argument for a single module
653 * passed in $module.
654 *
655 * <code>
656 * $PAGE->strings_for_js(Array('one', 'two', 'three'), 'mymod', Array('a', null, 3));
657 *
658 * // The above is identifical to calling
659 *
660 * $PAGE->string_for_js('one', 'mymod', 'a');
661 * $PAGE->string_for_js('two', 'mymod');
662 * $PAGE->string_for_js('three', 'mymod', 3);
663 * </code>
664 *
665 * @param array $identifiers An array of desired strings
666 * @param string $component The module to load for
667 * @param mixed $a This can either be a single variable that gets passed as extra
668 * information for every string or it can be an array of mixed data where the
669 * key for the data matches that of the identifier it is meant for.
670 *
671 */
672 public function strings_for_js($identifiers, $component, $a=NULL) {
673 foreach ($identifiers as $key => $identifier) {
674 if (is_array($a) && array_key_exists($key, $a)) {
675 $extra = $a[$key];
676 } else {
677 $extra = $a;
678 }
679 $this->string_for_js($identifier, $component, $extra);
680 }
681 }
682
683 /**
684 * !!!!!!DEPRECATED!!!!!! please use js_init_call() for everything now.
685 *
686 * Make some data from PHP available to JavaScript code.
687 *
688 * For example, if you call
689 * <pre>
690 * $PAGE->requires->data_for_js('mydata', array('name' => 'Moodle'));
691 * </pre>
692 * then in JavsScript mydata.name will be 'Moodle'.
693 * @param string $variable the the name of the JavaScript variable to assign the data to.
694 * Will probably work if you use a compound name like 'mybuttons.button[1]', but this
695 * should be considered an experimental feature.
696 * @param mixed $data The data to pass to JavaScript. This will be escaped using json_encode,
697 * so passing objects and arrays should work.
698 * @param bool $inhead initialise in head
699 * @return void
700 */
701 public function data_for_js($variable, $data, $inhead=false) {
702 $where = $inhead ? 'head' : 'footer';
703 $this->jsinitvariables[$where][] = array($variable, $data);
704 }
705
706 /**
707 * Creates a YUI event handler.
708 *
709 * @param mixed $selector standard YUI selector for elemnts, may be array or string, element id is in the form "#idvalue"
710 * @param string $event A valid DOM event (click, mousedown, change etc.)
711 * @param string $function The name of the function to call
712 * @param array $arguments An optional array of argument parameters to pass to the function
713 * @return void
714 */
715 public function event_handler($selector, $event, $function, array $arguments = null) {
716 $this->eventhandlers[] = array('selector'=>$selector, 'event'=>$event, 'function'=>$function, 'arguments'=>$arguments);
717 }
718
719 /**
720 * Returns code needed for registering of event handlers.
721 * @return string JS code
722 */
723 protected function get_event_handler_code() {
724 $output = '';
725 foreach ($this->eventhandlers as $h) {
726 $output .= js_writer::event_handler($h['selector'], $h['event'], $h['function'], $h['arguments']);
727 }
728 return $output;
729 }
730
731 /**
732 * Get the inline JavaScript code that need to appear in a particular place.
733 * @return bool $ondomready
734 */
735 protected function get_javascript_code($ondomready) {
736 $where = $ondomready ? 'ondomready' : 'normal';
737 $output = '';
738 if ($this->jscalls[$where]) {
739 foreach ($this->jscalls[$where] as $data) {
e6a3ea28 740 $output .= js_writer::function_call($data[0], $data[1], $data[2]);
0bb38e8c
PS
741 }
742 if (!empty($ondomready)) {
743 $output = " Y.on('domready', function() {\n$output\n });";
744 }
745 }
746 return $output;
747 }
748
749 /**
750 * Returns js code to be executed when Y is available.
751 * @return unknown_type
752 */
753 protected function get_javascript_init_code() {
754 if (count($this->jsinitcode)) {
755 return implode("\n", $this->jsinitcode) . "\n";
756 }
757 return '';
758 }
759
760 /**
761 * Returns basic YUI3 JS loading code.
762 * YUI3 is using autoloading of both CSS and JS code.
763 *
764 * Major benefit of this compared to standard js/csss loader is much improved
765 * caching, better browser cache utilisation, much fewer http requests.
766 *
767 * @return string
768 */
769 protected function get_yui3lib_headcode() {
770 $code = $this->yui3loader->css() . $this->yui3loader->script();
771 // unfortunately yui loader does not produce xhtml strict code, so let's fix it for now
772 $code = str_replace('&amp;', '&', $code);
773 $code = str_replace('&', '&amp;', $code);
774 return $code;
775 }
776
777 /**
778 * Returns basic YUI2 JS loading code.
779 * It can be called manually at any time.
780 * If called manually the result needs to be output using echo().
781 *
782 * Major benefit of this compared to standard js loader is much improved
783 * caching, better browser cache utilisation, much fewer http requests.
784 *
785 * All YUI2 CSS is loaded automatically.
786 *
787 * @return string JS embedding code
788 */
789 public function get_yui2lib_code() {
790 global $CFG;
791
792 if ($this->headdone) {
793 $code = $this->yui2loader->script();
794 } else {
795 $code = $this->yui2loader->script();
796 if ($this->yui2loader->combine) {
797 $skinurl = $this->yui2loader->comboBase . $CFG->yui2version . '/build/assets/skins/sam/skin.css';
798 } else {
799 $skinurl = $this->yui2loader->base . 'assets/skins/sam/skin.css';
800 }
801 // please note this is a temporary hack until we fully migrate to later YUI3 that has all the widgets
802 $attributes = array('rel'=>'stylesheet', 'type'=>'text/css', 'href'=>$skinurl);
803 $code .= "\n" . html_writer::empty_tag('link', $attributes) . "\n";
804 }
805 $code = str_replace('&amp;', '&', $code);
806 $code = str_replace('&', '&amp;', $code);
807 return $code;
808 }
809
810 /**
811 * Returns html tags needed for inclusion of theme CSS
812 * @return string
813 */
814 protected function get_css_code() {
815 // First of all the theme CSS, then any custom CSS
816 // Please note custom CSS is strongly discouraged,
817 // because it can not be overridden by themes!
818 // It is suitable only for things like mod/data which accepts CSS from teachers.
819
820 $code = '';
821 $attributes = array('id'=>'firstthemesheet', 'rel'=>'stylesheet', 'type'=>'text/css');
822
823 $urls = $this->cssthemeurls + $this->cssurls;
824
825 foreach ($urls as $url) {
826 $attributes['href'] = $url;
827 $code .= html_writer::empty_tag('link', $attributes) . "\n";
828 // this id is needed in first sheet only so that theme may override YUI sheets laoded on the fly
829 unset($attributes['id']);
830 }
831
832 return $code;
833 }
834
835 /**
836 * Adds extra modules specified after printing of page header
837 */
838 protected function get_extra_modules_code() {
839 if (empty($this->extramodules)) {
840 return '';
841 }
842 return html_writer::script(js_writer::function_call('M.yui.add_module', array($this->extramodules)));
843 }
844
845 /**
846 * Generate any HTML that needs to go inside the <head> tag.
847 *
848 * Normally, this method is called automatically by the code that prints the
849 * <head> tag. You should not normally need to call it in your own code.
850 *
851 * @return string the HTML code to to inside the <head> tag.
852 */
853 public function get_head_code(moodle_page $page, core_renderer $renderer) {
854 global $CFG;
855
856 // note: the $page and $output are not stored here because it would
857 // create circular references in memory which prevents garbage collection
858 $this->init_requirements_data($page, $renderer);
859
860 // yui3 JS and CSS is always loaded first - it is cached in browser
861 $output = $this->get_yui3lib_headcode();
862
863 // BC: load basic YUI2 for now, all yui2 things should be loaded via Y.use('yui2-oldmodulename')
864 $output .= $this->get_yui2lib_code();
865
866 // now theme CSS + custom CSS in this specific order
867 $output .= $this->get_css_code();
868
869 // set up global YUI3 loader object - this should contain all code needed by plugins
870 // note: in JavaScript just use "YUI(M.yui.loader).use('overlay', function(Y) { .... });"
871 // this needs to be done before including any other script
872 $js = "var M = {}; M.yui = {}; ";
873 $js .= js_writer::set_variable('M.yui.loader', $this->M_yui_loader, false) . "\n";
874 $js .= js_writer::set_variable('M.cfg', $this->M_cfg, false);
875 $output .= html_writer::script($js);
876
877 // link our main JS file, all core stuff should be there
0139ec3f 878 $output .= html_writer::script('', $this->js_fix_url('/lib/javascript-static.js'));
0bb38e8c
PS
879
880 // add variables
881 if ($this->jsinitvariables['head']) {
882 $js = '';
883 foreach ($this->jsinitvariables['head'] as $data) {
884 list($var, $value) = $data;
885 $js .= js_writer::set_variable($var, $value, true);
886 }
887 $output .= html_writer::script($js);
888 }
889
890 // all the other linked things from HEAD - there should be as few as possible
891 if ($this->jsincludes['head']) {
892 foreach ($this->jsincludes['head'] as $url) {
893 $output .= html_writer::script('', $url);
894 }
895 }
896
897 // mark head sending done, it is not possible to anything there
898 $this->headdone = true;
899
900 return $output;
901 }
902
903 /**
904 * Generate any HTML that needs to go at the start of the <body> tag.
905 *
906 * Normally, this method is called automatically by the code that prints the
907 * <head> tag. You should not normally need to call it in your own code.
908 *
909 * @return string the HTML code to go at the start of the <body> tag.
910 */
911 public function get_top_of_body_code() {
912 // first the skip links
913 $links = '';
914 $attributes = array('class'=>'skip');
915 foreach ($this->skiplinks as $url => $text) {
916 $attributes['href'] = '#' . $url;
26acc814 917 $links .= html_writer::tag('a', $text, $attributes);
0bb38e8c 918 }
26acc814 919 $output = html_writer::tag('div', $links, array('class'=>'skiplinks')) . "\n";
0bb38e8c
PS
920
921 // then the clever trick for hiding of things not needed when JS works
922 $output .= html_writer::script("document.body.className += ' jsenabled';") . "\n";
923 $this->topofbodydone = true;
924 return $output;
925 }
926
927 /**
928 * Generate any HTML that needs to go at the end of the page.
929 *
930 * Normally, this method is called automatically by the code that prints the
931 * page footer. You should not normally need to call it in your own code.
932 *
933 * @return string the HTML code to to at the end of the page.
934 */
935 public function get_end_code() {
936 global $CFG;
937 // add other requested modules
938 $output = $this->get_extra_modules_code();
939
940 // add missing YUI2 YUI - to be removed once we convert everything to YUI3!
941 $output .= $this->get_yui2lib_code();
942
943 // all the other linked scripts - there should be as few as possible
944 if ($this->jsincludes['footer']) {
945 foreach ($this->jsincludes['footer'] as $url) {
946 $output .= html_writer::script('', $url);
947 }
948 }
949
950 // add all needed strings
951 if (!empty($this->stringsforjs)) {
2b728cb5 952 $output .= html_writer::script(js_writer::set_variable('M.str', $this->stringsforjs));
0bb38e8c
PS
953 }
954
955 // add variables
956 if ($this->jsinitvariables['footer']) {
957 $js = '';
958 foreach ($this->jsinitvariables['footer'] as $data) {
959 list($var, $value) = $data;
960 $js .= js_writer::set_variable($var, $value, true);
961 }
962 $output .= html_writer::script($js);
963 }
964
965 $inyuijs = $this->get_javascript_code(false);
966 $ondomreadyjs = $this->get_javascript_code(true);
967 $jsinit = $this->get_javascript_init_code();
968 $handlersjs = $this->get_event_handler_code();
969
0139ec3f 970 // there is no global Y, make sure it is available in your scope
0bb38e8c
PS
971 $js = "YUI(M.yui.loader).use('node', function(Y) {\n{$inyuijs}{$ondomreadyjs}{$jsinit}{$handlersjs}\n});";
972
973 $output .= html_writer::script($js);
974
975 return $output;
976 }
977
978 /**
979 * @return boolean Have we already output the code in the <head> tag?
980 */
981 public function is_head_done() {
982 return $this->headdone;
983 }
984
985 /**
986 * @return boolean Have we already output the code at the start of the <body> tag?
987 */
988 public function is_top_of_body_done() {
989 return $this->topofbodydone;
990 }
991}
0139ec3f
PS
992
993/**
994 * Invalidate all server and client side JS caches.
995 * @return void
996 */
997function js_reset_all_caches() {
998 global $CFG;
999 require_once("$CFG->libdir/filelib.php");
1000
1001 set_config('jsrev', empty($CFG->jsrev) ? 1 : $CFG->jsrev+1);
1002 //fulldelete("$CFG->dataroot/cache/js");
1003}
1004