Scrub css id and css class on panes.
[project/panels.git] / plugins / display_renderers / panels_renderer_standard.class.php
1 <?php
2 // $Id$
3
4 /**
5 * The standard render pipeline for a Panels display object.
6 *
7 * Given a fully-loaded panels_display object, this class will turn its
8 * combination of layout, panes, and styles into HTML, invoking caching
9 * appropriately along the way. Interacting with the renderer externally is
10 * very simple - just pass it the display object and call the render() method:
11 *
12 * @code
13 * // given that $display is a fully loaded Panels display object
14 * $renderer = panels_get_renderer_handler('standard', $display)
15 * $html_output = $renderer->render();
16 * @endcode
17 *
18 * Internally, the render pipeline is divided into two phases, prepare and
19 * render:
20 * - The prepare phase transforms the skeletal data on the provided
21 * display object into a structure that is expected by the render phase.
22 * It is divided into a series of discrete sub-methods and operates
23 * primarily by passing parameters, all with the intention of making
24 * subclassing easier.
25 * - The render phase relies primarily on data stored in the renderer object's
26 * properties, presumably set in the prepare phase. It iterates through the
27 * rendering of each pane, pane styling, placement in panel regions, region
28 * styling, and finally the arrangement of rendered regions in the layout.
29 * Caching, if in use, is triggered per pane, or on the entire display.
30 *
31 * In short: prepare builds conf, render renders conf. Subclasses should respect
32 * this separation of responsibilities by adhering to these loose guidelines,
33 * given a loaded display object:
34 * - If your renderer needs to modify the datastructure representing what is
35 * to be rendered (panes and their conf, styles, caching, etc.), it should
36 * use the prepare phase.
37 * - If your renderer needs to modify the manner in which that renderable
38 * datastructure data is rendered, it should use the render phase.
39 *
40 * In the vast majority of use cases, this standard renderer will be sufficient
41 * and need not be switched out/subclassed; style and/or layout plugins can
42 * accommodate nearly every use case. If you think you might need a custom
43 * renderer, consider the following criteria/examples:
44 * - Some additional markup needs to be added to EVERY SINGLE panel.
45 * - Given a full display object, just render one pane.
46 * - Show a Panels admin interface.
47 *
48 * The system is almost functionally identical to the old procedural approach,
49 * with some exceptions (@see panels_renderer_legacy for details). The approach
50 * here differs primarily in its friendliness to tweaking in subclasses.
51 */
52 class panels_renderer_standard {
53 /**
54 * The fully-loaded Panels display object that is to be rendered. "Fully
55 * loaded" is defined as:
56 * 1. Having been produced by panels_load_displays(), whether or this page
57 * request or at some time in the past and the object was exported.
58 * 2. Having had some external code attach context data ($display->context),
59 * in the exact form expected by panes. Context matching is delicate,
60 * typically relying on exact string matches, so special attention must
61 * be taken.
62 *
63 * @var panels_display
64 */
65 var $display;
66
67 /**
68 * An associative array of loaded plugins. Used primarily as a central
69 * location for storing plugins that require additional loading beyond
70 * reading the plugin definition, which is already statically cached by
71 * ctools_get_plugins(). An example is layout plugins, which can optionally
72 * have a callback that determines the set of panel regions available at
73 * runtime.
74 *
75 * @var array
76 */
77 var $plugins = array();
78
79 /**
80 * A multilevel array of rendered data. The first level of the array
81 * indicates the type of rendered data, typically with up to three keys:
82 * 'layout', 'regions', and 'panes'. The relevant rendered data is stored as
83 * the value for each of these keys as it is generated:
84 * - 'panes' are an associative array of rendered output, keyed on pane id.
85 * - 'regions' are an associative array of rendered output, keyed on region
86 * name.
87 * - 'layout' is the whole of the rendered output.
88 *
89 * @var array
90 */
91 var $rendered = array();
92
93 /**
94 * A multilevel array of data prepared for rendering. The first level of the
95 * array indicates the type of prepared data. The standard renderer populates
96 * and uses two top-level keys, 'panes' and 'regions':
97 * - 'panes' are an associative array of pane objects to be rendered, keyed
98 * on pane id and sorted into proper rendering order.
99 * - 'regions' are an associative array of regions, keyed on region name,
100 * each of which is itself an indexed array of pane ids in the order in
101 * which those panes appear in that region.
102 *
103 * @var array
104 */
105 var $prepared = array();
106
107 /**
108 * Boolean state variable, indicating whether or not the prepare() method has
109 * been run.
110 *
111 * This state is checked in panels_renderer_standard::render_layout() to
112 * determine whether the prepare method should be automatically triggered.
113 *
114 * @var bool
115 */
116 var $prep_run = FALSE;
117
118 /**
119 * The plugin that defines this handler.
120 */
121 var $plugin = FALSE;
122
123 /**
124 * TRUE if this renderer is rendering in administrative mode
125 * which will allow layouts to have extra functionality.
126 *
127 * @var bool
128 */
129 var $admin = FALSE;
130
131 /**
132 * Where to add standard meta information. There are three possibilities:
133 * - standard: Put the meta information in the normal location. Default.
134 * - inline: Put the meta information directly inline. This will
135 * not work for javascript.
136 *
137 * @var string
138 */
139 var $meta_location = 'standard';
140
141 /**
142 * Include rendered HTML prior to the layout.
143 *
144 * @var string
145 */
146 var $prefix = '';
147
148 /**
149 * Include rendered HTML after the layout.
150 *
151 * @var string
152 */
153 var $suffix = '';
154
155 /**
156 * Receive and store the display object to be rendered.
157 *
158 * This is a psuedo-constructor that should typically be called immediately
159 * after object construction.
160 *
161 * @param array $plugin
162 * The definition of the renderer plugin.
163 * @param panels_display $display
164 * The panels display object to be rendered.
165 */
166 function init($plugin, &$display) {
167 $this->plugin = $plugin;
168 $layout = panels_get_layout($display->layout);
169 $this->display = &$display;
170 $this->plugins['layout'] = $layout;
171 if (!isset($layout['panels'])) {
172 $this->plugins['layout']['panels'] = panels_get_regions($layout, $display);
173 }
174
175 if (empty($this->plugins['layout'])) {
176 watchdog('panels', "Layout: @layout couldn't been found, maybe the theme is disabled.", array('@layout' => $display->layout));
177 }
178 }
179
180 /**
181 * Prepare the attached display for rendering.
182 *
183 * This is the outermost prepare method. It calls several sub-methods as part
184 * of the overall preparation process. This compartmentalization is intended
185 * to ease the task of modifying renderer behavior in child classes.
186 *
187 * If you override this method, it is important that you either call this
188 * method via parent::prepare(), or manually set $this->prep_run = TRUE.
189 *
190 * @param mixed $external_settings
191 * An optional parameter allowing external code to pass in additional
192 * settings for use in the preparation process. Not used in the default
193 * renderer, but included for interface consistency.
194 */
195 function prepare($external_settings = NULL) {
196 $this->prepare_panes($this->display->content);
197 $this->prepare_regions($this->display->panels, $this->display->panel_settings);
198 $this->prep_run = TRUE;
199 }
200
201 /**
202 * Prepare the list of panes to be rendered, accounting for visibility/access
203 * settings and rendering order.
204 *
205 * This method represents the standard approach for determining the list of
206 * panes to be rendered that is compatible with all parts of the Panels
207 * architecture. It first applies visibility & access checks, then sorts panes
208 * into their proper rendering order, and returns the result as an array.
209 *
210 * Inheriting classes should override this method if that renderer needs to
211 * regularly make additions to the set of panes that will be rendered.
212 *
213 * @param array $panes
214 * An associative array of pane data (stdClass objects), keyed on pane id.
215 * @return array
216 * An associative array of panes to be rendered, keyed on pane id and sorted
217 * into proper rendering order.
218 */
219 function prepare_panes($panes) {
220 ctools_include('content');
221 // Use local variables as writing to them is very slightly faster
222 $first = $normal = $last = array();
223
224 // Prepare the list of panes to be rendered
225 foreach ($panes as $pid => $pane) {
226 if (empty($this->admin)) {
227 // TODO remove in 7.x and ensure the upgrade path weeds out any stragglers; it's been long enough
228 $pane->shown = !empty($pane->shown); // guarantee this field exists.
229 // If this pane is not visible to the user, skip out and do the next one
230 if (!$pane->shown || !panels_pane_access($pane, $this->display)) {
231 continue;
232 }
233 }
234
235 $content_type = ctools_get_content_type($pane->type);
236
237 // If this pane wants to render last, add it to the $last array. We allow
238 // this because some panes need to be rendered after other panes,
239 // primarily so they can do things like the leftovers of forms.
240 if (!empty($content_type['render last'])) {
241 $last[$pid] = $pane;
242 }
243 // If it wants to render first, add it to the $first array. This is used
244 // by panes that need to do some processing before other panes are
245 // rendered.
246 else if (!empty($content_type['render first'])) {
247 $first[$pid] = $pane;
248 }
249 // Otherwise, render it in the normal order.
250 else {
251 $normal[$pid] = $pane;
252 }
253 }
254 $this->prepared['panes'] = $first + $normal + $last;
255 return $this->prepared['panes'];
256 }
257
258 /**
259 * Prepare the list of regions to be rendered.
260 *
261 * This method is primarily about properly initializing the style plugin that
262 * will be used to render the region. This is crucial as regions cannot be
263 * rendered without a style plugin (in keeping with Panels' philosophy of
264 * hardcoding none of its output), but for most regions no style has been
265 * explicitly set. The logic here is what accommodates that situation:
266 * - If a region has had its style explicitly set, then we fetch that plugin
267 * and continue.
268 * - If the region has no explicit style, but a style was set at the display
269 * level, then inherit the style from the display.
270 * - If neither the region nor the dispay have explicitly set styles, then
271 * fall back to the hardcoded 'default' style, a very minimal style.
272 *
273 * The other important task accomplished by this method is ensuring that even
274 * regions without any panes are still properly prepared for the rendering
275 * process. This is essential because the way Panels loads display objects
276 * (@see panels_load_displays) results only in a list of regions that
277 * contain panes - not necessarily all the regions defined by the layout
278 * plugin, which can only be determined by asking the plugin at runtime. This
279 * method consults that retrieved list of regions and prepares all of those,
280 * ensuring none are inadvertently skipped.
281 *
282 * @param array $region_pane_list
283 * An associative array of pane ids, keyed on the region to which those pids
284 * are assigned. In the default case, this is $display->panels.
285 * @param array $settings
286 * All known region style settings, including both the top-level display's
287 * settings (if any) and all region-specific settings (if any).
288 * @return array
289 * An array of regions prepared for rendering.
290 */
291 function prepare_regions($region_pane_list, $settings) {
292 // Initialize defaults to be used for regions without their own explicit
293 // settings. Use display settings if they exist, else hardcoded defaults.
294 $default = array(
295 'style' => panels_get_style(!empty($settings['style']) ? $settings['style'] : 'default'),
296 'style settings' => isset($settings['style_settings']['default']) ? $settings['style_settings']['default'] : array(),
297 );
298
299 $regions = array();
300 if (empty($settings)) {
301 // No display/panel region settings exist, init all with the defaults.
302 foreach ($this->plugins['layout']['panels'] as $region_id => $title) {
303 // Ensure this region has at least an empty panes array.
304 $panes = !empty($region_pane_list[$region_id]) ? $region_pane_list[$region_id] : array();
305
306 $regions[$region_id] = $default;
307 $regions[$region_id]['pids'] = $panes;
308 }
309 }
310 else {
311 // Some settings exist; iterate through each region and set individually.
312 foreach ($this->plugins['layout']['panels'] as $region_id => $title) {
313 // Ensure this region has at least an empty panes array.
314 $panes = !empty($region_pane_list[$region_id]) ? $region_pane_list[$region_id] : array();
315
316 if (empty($settings[$region_id]['style']) || $settings[$region_id]['style'] == -1) {
317 $regions[$region_id] = $default;
318 }
319 else {
320 $regions[$region_id]['style'] = panels_get_style($settings[$region_id]['style']);
321 $regions[$region_id]['style settings'] = isset($settings['style_settings'][$region_id]) ? $settings['style_settings'][$region_id] : array();
322 }
323 $regions[$region_id]['pids'] = $panes;
324 }
325 }
326
327 $this->prepared['regions'] = $regions;
328 return $this->prepared['regions'];
329 }
330
331 /**
332 * Build inner content, then hand off to layout-specified theme function for
333 * final render step.
334 *
335 * This is the outermost method in the Panels render pipeline. It calls the
336 * inner methods, which return a content array, which is in turn passed to the
337 * theme function specified in the layout plugin.
338 *
339 * @return string
340 * Themed & rendered HTML output.
341 */
342 function render() {
343 // Attach out-of-band data first.
344 $this->add_meta();
345
346 if (empty($this->display->cache['method']) || !empty($this->display->skip_cache)) {
347 return $this->render_layout();
348 }
349 else {
350 $cache = panels_get_cached_content($this->display, $this->display->args, $this->display->context);
351 if ($cache === FALSE) {
352 $cache = new panels_cache_object();
353 $cache->set_content($this->render_layout());
354 panels_set_cached_content($cache, $this->display, $this->display->args, $this->display->context);
355 }
356 return $cache->content;
357 }
358 }
359
360 /**
361 * Perform display/layout-level render operations.
362 *
363 * This method triggers all the inner pane/region rendering processes, passes
364 * that to the layout plugin's theme callback, and returns the rendered HTML.
365 *
366 * If display-level caching is enabled and that cache is warm, this method
367 * will not be called.
368 *
369 * @return string
370 * The HTML string representing the entire rendered, themed panel.
371 */
372 function render_layout() {
373 if (empty($this->prep_run)) {
374 $this->prepare();
375 }
376 $this->render_panes();
377 $this->render_regions();
378
379 if ($this->admin && !empty($this->plugins['layout']['admin theme'])) {
380 $theme = $this->plugins['layout']['admin theme'];
381 }
382 else {
383 $theme = $this->plugins['layout']['theme'];
384 }
385 $this->rendered['layout'] = theme($theme, check_plain($this->display->css_id), $this->rendered['regions'], $this->display->layout_settings, $this->display, $this->plugins['layout'], $this);
386 return $this->prefix . $this->rendered['layout'] . $this->suffix;
387 }
388
389 /**
390 * Attach out-of-band page metadata (e.g., CSS and JS).
391 *
392 * This must be done before render, because panels-within-panels must have
393 * their CSS added in the right order: inner content before outer content.
394 */
395 function add_meta() {
396 if (!empty($this->plugins['layout']['css'])) {
397 if (file_exists(path_to_theme() . '/' . $this->plugins['layout']['css'])) {
398 $this->add_css(path_to_theme() . '/' . $this->plugins['layout']['css']);
399 }
400 else {
401 $this->add_css($this->plugins['layout']['path'] . '/' . $this->plugins['layout']['css']);
402 }
403 }
404
405 if ($this->admin && isset($this->plugins['layout']['admin css'])) {
406 $this->add_css($this->plugins['layout']['path'] . '/' . $this->plugins['layout']['admin css']);
407 }
408 }
409
410 /**
411 * Add CSS information to the renderer.
412 *
413 * To facilitate previews over Views, CSS can now be added in a manner
414 * that does not necessarily mean just using drupal_add_css. Therefore,
415 * during the panel rendering process, this method can be used to add
416 * css and make certain that ti gets to the proper location.
417 *
418 * The arguments should exactly match drupal_add_css().
419 *
420 * @see drupal_add_css
421 */
422 function add_css($filename, $type = 'module', $media = 'all', $preprocess = TRUE) {
423 $path = file_create_path($filename);
424 switch ($this->meta_location) {
425 case 'standard':
426 if ($path) {
427 // Use CTools CSS add because it can handle temporary CSS in private
428 // filesystem.
429 ctools_include('css');
430 ctools_css_add_css($filename, $type, $media, $preprocess);
431 }
432 else {
433 drupal_add_css($filename, $type, $media, $preprocess);
434 }
435 break;
436 case 'inline':
437 if ($path) {
438 $url = file_create_url($filename);
439 }
440 else {
441 $url = base_path() . $filename;
442 }
443
444 $this->prefix .= '<link type="text/css" rel="stylesheet" media="' . $media . '" href="' . $url . '" />'."\n";
445 break;
446 }
447 }
448
449 /**
450 * Render all prepared panes, first by dispatching to their plugin's render
451 * callback, then handing that output off to the pane's style plugin.
452 *
453 * @return array
454 * The array of rendered panes, keyed on pane pid.
455 */
456 function render_panes() {
457 ctools_include('content');
458
459 // First, render all the panes into little boxes.
460 $this->rendered['panes'] = array();
461 foreach ($this->prepared['panes'] as $pid => $pane) {
462 $content = $this->render_pane($pane);
463 if ($content) {
464 $this->rendered['panes'][$pid] = $content;
465 }
466 }
467 return $this->rendered['panes'];
468 }
469
470 /**
471 * Render a pane using its designated style.
472 *
473 * This method also manages 'title pane' functionality, where the title from
474 * an individual pane can be bubbled up to take over the title for the entire
475 * display.
476 *
477 * @param stdClass $pane
478 * A Panels pane object, as loaded from the database.
479 */
480 function render_pane(&$pane) {
481 $content = $this->render_pane_content($pane);
482 if ($this->display->hide_title == PANELS_TITLE_PANE && !empty($this->display->title_pane) && $this->display->title_pane == $pane->pid) {
483
484 // If the user selected to override the title with nothing, and selected
485 // this as the title pane, assume the user actually wanted the original
486 // title to bubble up to the top but not actually be used on the pane.
487 if (empty($content->title) && !empty($content->original_title)) {
488 $this->display->stored_pane_title = $content->original_title;
489 }
490 else {
491 $this->display->stored_pane_title = !empty($content->title) ? $content->title : '';
492 }
493 }
494
495 if (!empty($content->content)) {
496 if (!empty($pane->style['style'])) {
497 $style = panels_get_style($pane->style['style']);
498
499 if (isset($style) && isset($style['render pane'])) {
500 $output = theme($style['render pane'], $content, $pane, $this->display, $style);
501
502 // This could be null if no theme function existed.
503 if (isset($output)) {
504 return $output;
505 }
506 }
507 }
508
509 // fallback
510 return theme('panels_pane', $content, $pane, $this->display);
511 }
512 }
513
514 /**
515 * Render the interior contents of a single pane.
516 *
517 * This method retrieves pane content and produces a ready-to-render content
518 * object. It also manages pane-specific caching.
519 *
520 * @param stdClass $pane
521 * A Panels pane object, as loaded from the database.
522 * @return stdClass $content
523 * A renderable object, containing a subject, content, etc. Based on the
524 * renderable objects used by the block system.
525 */
526 function render_pane_content(&$pane) {
527 ctools_include('context');
528 // TODO finally safe to remove this check?
529 if (!is_array($this->display->context)) {
530 watchdog('panels', 'renderer::render_pane_content() hit with a non-array for the context', $this->display, WATCHDOG_DEBUG);
531 $this->display->context = array();
532 }
533
534 $content = FALSE;
535 $caching = !empty($pane->cache['method']) && empty($this->display->skip_cache);
536 if ($caching && ($cache = panels_get_cached_content($this->display, $this->display->args, $this->display->context, $pane))) {
537 $content = $cache->content;
538 }
539 else {
540 $content = ctools_content_render($pane->type, $pane->subtype, $pane->configuration, array(), $this->display->args, $this->display->context);
541 foreach (module_implements('panels_pane_content_alter') as $module) {
542 $function = $module . '_panels_pane_content_alter';
543 $function($content, $pane, $this->display->args, $this->display->context);
544 }
545 if ($caching) {
546 $cache = new panels_cache_object();
547 $cache->set_content($content);
548 panels_set_cached_content($cache, $this->display, $this->display->args, $this->display->context, $pane);
549 $content = $cache->content;
550 }
551 }
552
553 // Pass long the css_id that is usually available.
554 if (!empty($pane->css['css_id'])) {
555 $content->css_id = check_plain($pane->css['css_id']);
556 }
557
558 // Pass long the css_class that is usually available.
559 if (!empty($pane->css['css_class'])) {
560 $content->css_class = check_plain($pane->css['css_class']);
561 }
562
563 return $content;
564 }
565
566 /**
567 * Render all prepared regions, placing already-rendered panes into their
568 * appropriate positions therein.
569 *
570 * @return array
571 * An array of rendered panel regions, keyed on the region name.
572 */
573 function render_regions() {
574 $this->rendered['regions'] = array();
575
576 // Loop through all panel regions, put all panes that belong to the current
577 // region in an array, then render the region. Primarily this ensures that
578 // the panes are arranged in the proper order.
579 $content = array();
580 foreach ($this->prepared['regions'] as $region_id => $conf) {
581 $region_panes = array();
582 foreach ($conf['pids'] as $pid) {
583 // Only include panes for region rendering if they had some output.
584 if (!empty($this->rendered['panes'][$pid])) {
585 $region_panes[$pid] = $this->rendered['panes'][$pid];
586 }
587 }
588 $this->rendered['regions'][$region_id] = $this->render_region($region_id, $region_panes);
589 }
590
591 return $this->rendered['regions'];
592 }
593
594 /**
595 * Render a single panel region.
596 *
597 * Primarily just a passthrough to the panel region rendering callback
598 * specified by the style plugin that is attached to the current panel region.
599 *
600 * @param $region_id
601 * The ID of the panel region being rendered
602 * @param $panes
603 * An array of panes that are assigned to the panel that's being rendered.
604 *
605 * @return string
606 * The rendered, HTML string output of the passed-in panel region.
607 */
608 function render_region($region_id, $panes) {
609 $style = $this->prepared['regions'][$region_id]['style'];
610 $style_settings = $this->prepared['regions'][$region_id]['style settings'];
611
612 // Retrieve the pid (can be a panel page id, a mini panel id, etc.), this
613 // might be used (or even necessary) for some panel display styles.
614 // TODO: Got to fix this to use panel page name instead of pid, since pid is
615 // no longer guaranteed. This needs an API to be able to set the final id.
616 $owner_id = 0;
617 if (isset($this->display->owner) && is_object($this->display->owner) && isset($this->display->owner->id)) {
618 $owner_id = $this->display->owner->id;
619 }
620
621 return theme($style['render region'], $this->display, $owner_id, $panes, $style_settings, $region_id, $style);
622 }
623 }