views_plugin_style.inc

  1. 3.x plugins/views_plugin_style.inc
  2. 2.x plugins/views_plugin_style.inc

Definition of views_plugin_style.

File

plugins/views_plugin_style.inc
View source
  1. <?php
  2. /**
  3. * @file
  4. * Definition of views_plugin_style.
  5. */
  6. /**
  7. * @defgroup views_style_plugins Views style plugins
  8. * @{
  9. * Style plugins control how a view is rendered. For example, they
  10. * can choose to display a collection of fields, node_view() output,
  11. * table output, or any kind of crazy output they want.
  12. *
  13. * Many style plugins can have an optional 'row' plugin, that displays
  14. * a single record. Not all style plugins can utilize this, so it is
  15. * up to the plugin to set this up and call through to the row plugin.
  16. *
  17. * @see hook_views_plugins()
  18. */
  19. /**
  20. * Base class to define a style plugin handler.
  21. */
  22. class views_plugin_style extends views_plugin {
  23. /**
  24. * Store all available tokens row rows.
  25. */
  26. var $row_tokens = array();
  27. /**
  28. * Contains the row plugin, if it's initialized
  29. * and the style itself supports it.
  30. *
  31. * @var views_plugin_row
  32. */
  33. var $row_plugin;
  34. /**
  35. * Initialize a style plugin.
  36. *
  37. * @param $view
  38. * @param $display
  39. * @param $options
  40. * The style options might come externally as the style can be sourced
  41. * from at least two locations. If it's not included, look on the display.
  42. */
  43. function init(&$view, &$display, $options = NULL) {
  44. $this->view = &$view;
  45. $this->display = &$display;
  46. // Overlay incoming options on top of defaults
  47. $this->unpack_options($this->options, isset($options) ? $options : $display->handler->get_option('style_options'));
  48. if ($this->uses_row_plugin() && $display->handler->get_option('row_plugin')) {
  49. $this->row_plugin = $display->handler->get_plugin('row');
  50. }
  51. $this->options += array(
  52. 'grouping' => array(),
  53. );
  54. $this->definition += array(
  55. 'uses grouping' => TRUE,
  56. );
  57. }
  58. function destroy() {
  59. parent::destroy();
  60. if (isset($this->row_plugin)) {
  61. $this->row_plugin->destroy();
  62. }
  63. }
  64. /**
  65. * Return TRUE if this style also uses a row plugin.
  66. */
  67. function uses_row_plugin() {
  68. return !empty($this->definition['uses row plugin']);
  69. }
  70. /**
  71. * Return TRUE if this style also uses a row plugin.
  72. */
  73. function uses_row_class() {
  74. return !empty($this->definition['uses row class']);
  75. }
  76. /**
  77. * Return TRUE if this style also uses fields.
  78. *
  79. * @return bool
  80. */
  81. function uses_fields() {
  82. // If we use a row plugin, ask the row plugin. Chances are, we don't
  83. // care, it does.
  84. $row_uses_fields = FALSE;
  85. if ($this->uses_row_plugin() && !empty($this->row_plugin)) {
  86. $row_uses_fields = $this->row_plugin->uses_fields();
  87. }
  88. // Otherwise, check the definition or the option.
  89. return $row_uses_fields || !empty($this->definition['uses fields']) || !empty($this->options['uses_fields']);
  90. }
  91. /**
  92. * Return TRUE if this style uses tokens.
  93. *
  94. * Used to ensure we don't fetch tokens when not needed for performance.
  95. */
  96. function uses_tokens() {
  97. if ($this->uses_row_class()) {
  98. $class = $this->options['row_class'];
  99. if (strpos($class, '[') !== FALSE || strpos($class, '!') !== FALSE || strpos($class, '%') !== FALSE) {
  100. return TRUE;
  101. }
  102. }
  103. }
  104. /**
  105. * Return the token replaced row class for the specified row.
  106. */
  107. function get_row_class($row_index) {
  108. if ($this->uses_row_class()) {
  109. $class = $this->options['row_class'];
  110. if ($this->uses_fields() && $this->view->field) {
  111. $class = strip_tags($this->tokenize_value($class, $row_index));
  112. }
  113. $classes = explode(' ', $class);
  114. foreach ($classes as &$class) {
  115. $class = drupal_clean_css_identifier($class);
  116. }
  117. return implode(' ', $classes);
  118. }
  119. }
  120. /**
  121. * Take a value and apply token replacement logic to it.
  122. */
  123. function tokenize_value($value, $row_index) {
  124. if (strpos($value, '[') !== FALSE || strpos($value, '!') !== FALSE || strpos($value, '%') !== FALSE) {
  125. $fake_item = array(
  126. 'alter_text' => TRUE,
  127. 'text' => $value,
  128. );
  129. // Row tokens might be empty, for example for node row style.
  130. $tokens = isset($this->row_tokens[$row_index]) ? $this->row_tokens[$row_index] : array();
  131. if (!empty($this->view->build_info['substitutions'])) {
  132. $tokens += $this->view->build_info['substitutions'];
  133. }
  134. if ($tokens) {
  135. $value = strtr($value, $tokens);
  136. }
  137. }
  138. return $value;
  139. }
  140. /**
  141. * Should the output of the style plugin be rendered even if it's a empty view.
  142. */
  143. function even_empty() {
  144. return !empty($this->definition['even empty']);
  145. }
  146. function option_definition() {
  147. $options = parent::option_definition();
  148. $options['grouping'] = array('default' => array());
  149. if ($this->uses_row_class()) {
  150. $options['row_class'] = array('default' => '');
  151. $options['default_row_class'] = array('default' => TRUE, 'bool' => TRUE);
  152. $options['row_class_special'] = array('default' => TRUE, 'bool' => TRUE);
  153. }
  154. $options['uses_fields'] = array('default' => FALSE, 'bool' => TRUE);
  155. return $options;
  156. }
  157. function options_form(&$form, &$form_state) {
  158. parent::options_form($form, $form_state);
  159. // Only fields-based views can handle grouping. Style plugins can also exclude
  160. // themselves from being groupable by setting their "use grouping" definiton
  161. // key to FALSE.
  162. // @TODO: Document "uses grouping" in docs.php when docs.php is written.
  163. if ($this->uses_fields() && $this->definition['uses grouping']) {
  164. $options = array('' => t('- None -'));
  165. $field_labels = $this->display->handler->get_field_labels(TRUE);
  166. $options += $field_labels;
  167. // If there are no fields, we can't group on them.
  168. if (count($options) > 1) {
  169. // This is for backward compability, when there was just a single select form.
  170. if (is_string($this->options['grouping'])) {
  171. $grouping = $this->options['grouping'];
  172. $this->options['grouping'] = array();
  173. $this->options['grouping'][0]['field'] = $grouping;
  174. }
  175. if (isset($this->options['group_rendered']) && is_string($this->options['group_rendered'])) {
  176. $this->options['grouping'][0]['rendered'] = $this->options['group_rendered'];
  177. unset($this->options['group_rendered']);
  178. }
  179. $c = count($this->options['grouping']);
  180. // Add a form for every grouping, plus one.
  181. for ($i = 0; $i <= $c; $i++) {
  182. $grouping = !empty($this->options['grouping'][$i]) ? $this->options['grouping'][$i] : array();
  183. $grouping += array('field' => '', 'rendered' => TRUE, 'rendered_strip' => FALSE);
  184. $form['grouping'][$i]['field'] = array(
  185. '#type' => 'select',
  186. '#title' => t('Grouping field Nr.@number', array('@number' => $i + 1)),
  187. '#options' => $options,
  188. '#default_value' => $grouping['field'],
  189. '#description' => t('You may optionally specify a field by which to group the records. Leave blank to not group.'),
  190. );
  191. $form['grouping'][$i]['rendered'] = array(
  192. '#type' => 'checkbox',
  193. '#title' => t('Use rendered output to group rows'),
  194. '#default_value' => $grouping['rendered'],
  195. '#description' => t('If enabled the rendered output of the grouping field is used to group the rows.'),
  196. '#dependency' => array(
  197. 'edit-style-options-grouping-' . $i . '-field' => array_keys($field_labels),
  198. )
  199. );
  200. $form['grouping'][$i]['rendered_strip'] = array(
  201. '#type' => 'checkbox',
  202. '#title' => t('Remove tags from rendered output'),
  203. '#default_value' => $grouping['rendered_strip'],
  204. '#dependency' => array(
  205. 'edit-style-options-grouping-' . $i . '-field' => array_keys($field_labels),
  206. )
  207. );
  208. }
  209. }
  210. }
  211. if ($this->uses_row_class()) {
  212. $form['row_class'] = array(
  213. '#title' => t('Row class'),
  214. '#description' => t('The class to provide on each row.'),
  215. '#type' => 'textfield',
  216. '#default_value' => $this->options['row_class'],
  217. );
  218. if ($this->uses_fields()) {
  219. $form['row_class']['#description'] .= ' ' . t('You may use field tokens from as per the "Replacement patterns" used in "Rewrite the output of this field" for all fields.');
  220. }
  221. $form['default_row_class'] = array(
  222. '#title' => t('Add views row classes'),
  223. '#description' => t('Add the default row classes like views-row-1 to the output. You can use this to quickly reduce the amount of markup the view provides by default, at the cost of making it more difficult to apply CSS.'),
  224. '#type' => 'checkbox',
  225. '#default_value' => $this->options['default_row_class'],
  226. );
  227. $form['row_class_special'] = array(
  228. '#title' => t('Add striping (odd/even), first/last row classes'),
  229. '#description' => t('Add css classes to the first and last line, as well as odd/even classes for striping.'),
  230. '#type' => 'checkbox',
  231. '#default_value' => $this->options['row_class_special'],
  232. );
  233. }
  234. if (!$this->uses_fields() || !empty($this->options['uses_fields'])) {
  235. $form['uses_fields'] = array(
  236. '#type' => 'checkbox',
  237. '#title' => t('Force using fields'),
  238. '#description' => t('If neither the row nor the style plugin supports fields, this field allows to enable them, so you can for example use groupby.'),
  239. '#default_value' => $this->options['uses_fields'],
  240. );
  241. }
  242. }
  243. function options_validate(&$form, &$form_state) {
  244. // Don't run validation on style plugins without the grouping setting.
  245. if (isset($form_state['values']['style_options']['grouping'])) {
  246. // Don't save grouping if no field is specified.
  247. foreach ($form_state['values']['style_options']['grouping'] as $index => $grouping) {
  248. if (empty($grouping['field'])) {
  249. unset($form_state['values']['style_options']['grouping'][$index]);
  250. }
  251. }
  252. }
  253. }
  254. /**
  255. * Called by the view builder to see if this style handler wants to
  256. * interfere with the sorts. If so it should build; if it returns
  257. * any non-TRUE value, normal sorting will NOT be added to the query.
  258. */
  259. function build_sort() { return TRUE; }
  260. /**
  261. * Called by the view builder to let the style build a second set of
  262. * sorts that will come after any other sorts in the view.
  263. */
  264. function build_sort_post() { }
  265. /**
  266. * Allow the style to do stuff before each row is rendered.
  267. *
  268. * @param $result
  269. * The full array of results from the query.
  270. */
  271. function pre_render($result) {
  272. if (!empty($this->row_plugin)) {
  273. $this->row_plugin->pre_render($result);
  274. }
  275. }
  276. /**
  277. * Render the display in this style.
  278. */
  279. function render() {
  280. if ($this->uses_row_plugin() && empty($this->row_plugin)) {
  281. debug('views_plugin_style_default: Missing row plugin');
  282. return;
  283. }
  284. // Group the rows according to the grouping instructions, if specified.
  285. $sets = $this->render_grouping(
  286. $this->view->result,
  287. $this->options['grouping'],
  288. TRUE
  289. );
  290. return $this->render_grouping_sets($sets);
  291. }
  292. /**
  293. * Render the grouping sets.
  294. *
  295. * Plugins may override this method if they wish some other way of handling
  296. * grouping.
  297. *
  298. * @param $sets
  299. * Array containing the grouping sets to render.
  300. * @param $level
  301. * Integer indicating the hierarchical level of the grouping.
  302. *
  303. * @return string
  304. * Rendered output of given grouping sets.
  305. */
  306. function render_grouping_sets($sets, $level = 0) {
  307. $output = '';
  308. foreach ($sets as $set) {
  309. $row = reset($set['rows']);
  310. // Render as a grouping set.
  311. if (is_array($row) && isset($row['group'])) {
  312. $output .= theme(views_theme_functions('views_view_grouping', $this->view, $this->display),
  313. array(
  314. 'view' => $this->view,
  315. 'grouping' => $this->options['grouping'][$level],
  316. 'grouping_level' => $level,
  317. 'rows' => $set['rows'],
  318. 'title' => $set['group'])
  319. );
  320. }
  321. // Render as a record set.
  322. else {
  323. if ($this->uses_row_plugin()) {
  324. foreach ($set['rows'] as $index => $row) {
  325. $this->view->row_index = $index;
  326. $set['rows'][$index] = $this->row_plugin->render($row);
  327. }
  328. }
  329. $output .= theme($this->theme_functions(),
  330. array(
  331. 'view' => $this->view,
  332. 'options' => $this->options,
  333. 'grouping_level' => $level,
  334. 'rows' => $set['rows'],
  335. 'title' => $set['group'])
  336. );
  337. }
  338. }
  339. unset($this->view->row_index);
  340. return $output;
  341. }
  342. /**
  343. * Group records as needed for rendering.
  344. *
  345. * @param $records
  346. * An array of records from the view to group.
  347. * @param $groupings
  348. * An array of grouping instructions on which fields to group. If empty, the
  349. * result set will be given a single group with an empty string as a label.
  350. * @param $group_rendered
  351. * Boolean value whether to use the rendered or the raw field value for
  352. * grouping. If set to NULL the return is structured as before
  353. * Views 7.x-3.0-rc2. After Views 7.x-3.0 this boolean is only used if
  354. * $groupings is an old-style string or if the rendered option is missing
  355. * for a grouping instruction.
  356. * @return
  357. * The grouped record set.
  358. * A nested set structure is generated if multiple grouping fields are used.
  359. *
  360. * @code
  361. * array(
  362. * 'grouping_field_1:grouping_1' => array(
  363. * 'group' => 'grouping_field_1:content_1',
  364. * 'rows' => array(
  365. * 'grouping_field_2:grouping_a' => array(
  366. * 'group' => 'grouping_field_2:content_a',
  367. * 'rows' => array(
  368. * $row_index_1 => $row_1,
  369. * $row_index_2 => $row_2,
  370. * // ...
  371. * )
  372. * ),
  373. * ),
  374. * ),
  375. * 'grouping_field_1:grouping_2' => array(
  376. * // ...
  377. * ),
  378. * )
  379. * @endcode
  380. */
  381. function render_grouping($records, $groupings = array(), $group_rendered = NULL) {
  382. // This is for backward compability, when $groupings was a string containing
  383. // the ID of a single field.
  384. if (is_string($groupings)) {
  385. $rendered = $group_rendered === NULL ? TRUE : $group_rendered;
  386. $groupings = array(array('field' => $groupings, 'rendered' => $rendered));
  387. }
  388. // Make sure fields are rendered
  389. $this->render_fields($this->view->result);
  390. $sets = array();
  391. if ($groupings) {
  392. foreach ($records as $index => $row) {
  393. // Iterate through configured grouping fields to determine the
  394. // hierarchically positioned set where the current row belongs to.
  395. // While iterating, parent groups, that do not exist yet, are added.
  396. $set = &$sets;
  397. foreach ($groupings as $info) {
  398. $field = $info['field'];
  399. $rendered = isset($info['rendered']) ? $info['rendered'] : $group_rendered;
  400. $rendered_strip = isset($info['rendered_strip']) ? $info['rendered_strip'] : FALSE;
  401. $grouping = '';
  402. $group_content = '';
  403. // Group on the rendered version of the field, not the raw. That way,
  404. // we can control any special formatting of the grouping field through
  405. // the admin or theme layer or anywhere else we'd like.
  406. if (isset($this->view->field[$field])) {
  407. $group_content = $this->get_field($index, $field);
  408. if ($this->view->field[$field]->options['label']) {
  409. $group_content = $this->view->field[$field]->options['label'] . ': ' . $group_content;
  410. }
  411. if ($rendered) {
  412. $grouping = $group_content;
  413. if ($rendered_strip) {
  414. $group_content = $grouping = strip_tags(htmlspecialchars_decode($group_content));
  415. }
  416. }
  417. else {
  418. $grouping = $this->get_field_value($index, $field);
  419. // Not all field handlers return a scalar value,
  420. // e.g. views_handler_field_field.
  421. if (!is_scalar($grouping)) {
  422. $grouping = md5(serialize($grouping));
  423. }
  424. }
  425. }
  426. // Create the group if it does not exist yet.
  427. if (empty($set[$grouping])) {
  428. $set[$grouping]['group'] = $group_content;
  429. $set[$grouping]['rows'] = array();
  430. }
  431. // Move the set reference into the row set of the group we just determined.
  432. $set = &$set[$grouping]['rows'];
  433. }
  434. // Add the row to the hierarchically positioned row set we just determined.
  435. $set[$index] = $row;
  436. }
  437. }
  438. else {
  439. // Create a single group with an empty grouping field.
  440. $sets[''] = array(
  441. 'group' => '',
  442. 'rows' => $records,
  443. );
  444. }
  445. // If this parameter isn't explicitely set modify the output to be fully
  446. // backward compatible to code before Views 7.x-3.0-rc2.
  447. // @TODO Remove this as soon as possible e.g. October 2020
  448. if ($group_rendered === NULL) {
  449. $old_style_sets = array();
  450. foreach ($sets as $group) {
  451. $old_style_sets[$group['group']] = $group['rows'];
  452. }
  453. $sets = $old_style_sets;
  454. }
  455. return $sets;
  456. }
  457. /**
  458. * Render all of the fields for a given style and store them on the object.
  459. *
  460. * @param $result
  461. * The result array from $view->result
  462. */
  463. function render_fields($result) {
  464. if (!$this->uses_fields()) {
  465. return;
  466. }
  467. if (!isset($this->rendered_fields)) {
  468. $this->rendered_fields = array();
  469. $this->view->row_index = 0;
  470. $keys = array_keys($this->view->field);
  471. // If all fields have a field::access FALSE there might be no fields, so
  472. // there is no reason to execute this code.
  473. if (!empty($keys)) {
  474. foreach ($result as $count => $row) {
  475. $this->view->row_index = $count;
  476. foreach ($keys as $id) {
  477. $this->rendered_fields[$count][$id] = $this->view->field[$id]->theme($row);
  478. }
  479. $this->row_tokens[$count] = $this->view->field[$id]->get_render_tokens(array());
  480. }
  481. }
  482. unset($this->view->row_index);
  483. }
  484. return $this->rendered_fields;
  485. }
  486. /**
  487. * Get a rendered field.
  488. *
  489. * @param $index
  490. * The index count of the row.
  491. * @param $field
  492. * The id of the field.
  493. */
  494. function get_field($index, $field) {
  495. if (!isset($this->rendered_fields)) {
  496. $this->render_fields($this->view->result);
  497. }
  498. if (isset($this->rendered_fields[$index][$field])) {
  499. return $this->rendered_fields[$index][$field];
  500. }
  501. }
  502. /**
  503. * Get the raw field value.
  504. *
  505. * @param $index
  506. * The index count of the row.
  507. * @param $field
  508. * The id of the field.
  509. */
  510. function get_field_value($index, $field) {
  511. $this->view->row_index = $index;
  512. $value = $this->view->field[$field]->get_value($this->view->result[$index]);
  513. unset($this->view->row_index);
  514. return $value;
  515. }
  516. function validate() {
  517. $errors = parent::validate();
  518. if ($this->uses_row_plugin()) {
  519. $plugin = $this->display->handler->get_plugin('row');
  520. if (empty($plugin)) {
  521. $errors[] = t('Style @style requires a row style but the row plugin is invalid.', array('@style' => $this->definition['title']));
  522. }
  523. else {
  524. $result = $plugin->validate();
  525. if (!empty($result) && is_array($result)) {
  526. $errors = array_merge($errors, $result);
  527. }
  528. }
  529. }
  530. return $errors;
  531. }
  532. function query() {
  533. parent::query();
  534. if (isset($this->row_plugin)) {
  535. $this->row_plugin->query();
  536. }
  537. }
  538. }
  539. /**
  540. * @}
  541. */