1<?php 2 3namespace dokuwiki\plugin\struct\meta; 4 5/** 6 * Creates the table aggregation output 7 * 8 * @package dokuwiki\plugin\struct\meta 9 */ 10class AggregationTable extends Aggregation 11{ 12 /** @var array for summing up columns */ 13 protected $sums; 14 15 /** @var string[] the result PIDs for each row */ 16 protected $resultPIDs; 17 protected $resultRids; 18 protected $resultRevs; 19 20 public function __construct($id, $mode, \Doku_Renderer $renderer, SearchConfig $searchConfig) 21 { 22 parent::__construct($id, $mode, $renderer, $searchConfig); 23 $this->resultPIDs = $this->searchConfig->getPids(); 24 $this->resultRids = $this->searchConfig->getRids(); 25 $this->resultRevs = $this->searchConfig->getRevs(); 26 } 27 28 /** @inheritdoc */ 29 public function render($showNotFound = false) 30 { 31 32 // abort early if there are no results at all (not filtered) 33 if (!$this->resultCount && !$this->isDynamicallyFiltered() && $showNotFound) { 34 $this->startScope(); 35 $this->renderer->cdata($this->helper->getLang('none')); 36 $this->finishScope(); 37 return; 38 } 39 40 $this->startScope(); 41 $this->renderActiveFilters(); 42 43 $rendercontext = array( 44 'table' => $this, 45 'renderer' => $this->renderer, 46 'format' => $this->mode, 47 'search' => $this->searchConfig, 48 'columns' => $this->columns, 49 'data' => $this->result 50 ); 51 52 $event = new \Doku_Event( 53 'PLUGIN_STRUCT_RENDER_AGGREGATION_TABLE', 54 $rendercontext 55 ); 56 $event->trigger([$this, 'renderTable']); 57 58 // export handle 59 $this->renderExportControls(); 60 $this->finishScope(); 61 } 62 63 /** 64 * Render the default aggregation table 65 */ 66 public function renderTable($rendercontext) 67 { 68 $this->renderer->table_open(); 69 70 // header 71 $this->renderer->tablethead_open(); 72 $this->renderColumnHeaders(); 73 $this->renderDynamicFilters(); 74 $this->renderer->tablethead_close(); 75 76 if ($this->resultCount) { 77 // actual data 78 $this->renderer->tabletbody_open(); 79 $this->renderResult(); 80 $this->renderer->tabletbody_close(); 81 82 // footer (tfoot is develonly currently) 83 if (method_exists($this->renderer, 'tabletfoot_open')) $this->renderer->tabletfoot_open(); 84 $this->renderSums(); 85 $this->renderPagingControls(); 86 if (method_exists($this->renderer, 'tabletfoot_close')) $this->renderer->tabletfoot_close(); 87 } else { 88 // nothing found 89 $this->renderEmptyResult(); 90 } 91 92 // table close 93 $this->renderer->table_close(); 94 } 95 96 /** 97 * Adds additional info to document and renderer in XHTML mode 98 * 99 * @see finishScope() 100 */ 101 protected function startScope() 102 { 103 // unique identifier for this aggregation 104 $this->renderer->info['struct_table_hash'] = md5(var_export($this->data, true)); 105 106 // wrapping div 107 if ($this->mode != 'xhtml') return; 108 $this->renderer->doc .= "<div class=\"structaggregation\">"; 109 } 110 111 /** 112 * Closes the table and anything opened in startScope() 113 * 114 * @see startScope() 115 */ 116 protected function finishScope() 117 { 118 // remove identifier from renderer again 119 if (isset($this->renderer->info['struct_table_hash'])) { 120 unset($this->renderer->info['struct_table_hash']); 121 } 122 123 // wrapping div 124 if ($this->mode != 'xhtml') return; 125 $this->renderer->doc .= '</div>'; 126 } 127 128 /** 129 * Displays info about the currently applied filters 130 */ 131 protected function renderActiveFilters() 132 { 133 if ($this->mode != 'xhtml') return; 134 $dynamic = $this->searchConfig->getDynamicParameters(); 135 $filters = $dynamic->getFilters(); 136 if (!$filters) return; 137 138 $fltrs = array(); 139 foreach ($filters as $column => $filter) { 140 list($comp, $value) = $filter; 141 142 // display the filters in a human readable format 143 foreach ($this->columns as $col) { 144 if ($column === $col->getFullQualifiedLabel()) { 145 $column = $col->getTranslatedLabel(); 146 } 147 } 148 $fltrs[] = sprintf('"%s" %s "%s"', $column, $this->helper->getLang("comparator $comp"), $value); 149 } 150 151 $this->renderer->doc .= '<div class="filter">'; 152 $this->renderer->doc .= '<h4>' . 153 sprintf( 154 $this->helper->getLang('tablefilteredby'), 155 hsc(implode(' & ', $fltrs)) 156 ) . 157 '</h4>'; 158 $this->renderer->doc .= '<div class="resetfilter">'; 159 $this->renderer->internallink($this->id, $this->helper->getLang('tableresetfilter')); 160 $this->renderer->doc .= '</div>'; 161 $this->renderer->doc .= '</div>'; 162 } 163 164 /** 165 * Shows the column headers with links to sort by column 166 */ 167 protected function renderColumnHeaders() 168 { 169 $this->renderer->tablerow_open(); 170 171 // additional column for row numbers 172 if (!empty($this->data['rownumbers'])) { 173 $this->renderer->tableheader_open(); 174 $this->renderer->cdata('#'); 175 $this->renderer->tableheader_close(); 176 } 177 178 // show all headers 179 foreach ($this->columns as $num => $column) { 180 $header = ''; 181 if (isset($this->data['headers'][$num])) { 182 $header = $this->data['headers'][$num]; 183 } 184 185 // use field label if no header was set 186 if (blank($header)) { 187 if (is_a($column, 'dokuwiki\plugin\struct\meta\Column')) { 188 $header = $column->getTranslatedLabel(); 189 } else { 190 $header = 'column ' . $num; // this should never happen 191 } 192 } 193 194 // simple mode first 195 if ($this->mode != 'xhtml') { 196 $this->renderer->tableheader_open(); 197 $this->renderer->cdata($header); 198 $this->renderer->tableheader_close(); 199 continue; 200 } 201 202 // still here? create custom header for more flexibility 203 204 // width setting, widths are prevalidated, no escape needed 205 $width = ''; 206 if (isset($this->data['widths'][$num]) && $this->data['widths'][$num] != '-') { 207 $width = ' style="min-width: ' . $this->data['widths'][$num] . ';' . 208 'max-width: ' . $this->data['widths'][$num] . ';"'; 209 } 210 211 // prepare data attribute for inline edits 212 if ( 213 !is_a($column, '\dokuwiki\plugin\struct\meta\PageColumn') && 214 !is_a($column, '\dokuwiki\plugin\struct\meta\RevisionColumn') 215 ) { 216 $data = 'data-field="' . hsc($column->getFullQualifiedLabel()) . '"'; 217 } else { 218 $data = ''; 219 } 220 221 // sort indicator and link 222 $sortclass = ''; 223 $sorts = $this->searchConfig->getSorts(); 224 $dynamic = $this->searchConfig->getDynamicParameters(); 225 $dynamic->setSort($column, true); 226 if (isset($sorts[$column->getFullQualifiedLabel()])) { 227 list(/*colname*/, $currentSort) = $sorts[$column->getFullQualifiedLabel()]; 228 if ($currentSort) { 229 $sortclass = 'sort-down'; 230 $dynamic->setSort($column, false); 231 } else { 232 $sortclass = 'sort-up'; 233 } 234 } 235 $link = wl($this->id, $dynamic->getURLParameters()); 236 237 // output XHTML header 238 $this->renderer->doc .= "<th $width $data>"; 239 $this->renderer->doc .= '<a href="' . $link . '" class="' . $sortclass . '" ' . 240 'title="' . $this->helper->getLang('sort') . '">' . hsc($header) . '</a>'; 241 $this->renderer->doc .= '</th>'; 242 } 243 244 $this->renderer->tablerow_close(); 245 } 246 247 /** 248 * Is the result set currently dynamically filtered? 249 * @return bool 250 */ 251 protected function isDynamicallyFiltered() 252 { 253 if ($this->mode != 'xhtml') return false; 254 if (!$this->data['dynfilters']) return false; 255 256 $dynamic = $this->searchConfig->getDynamicParameters(); 257 return (bool)$dynamic->getFilters(); 258 } 259 260 /** 261 * Add input fields for dynamic filtering 262 */ 263 protected function renderDynamicFilters() 264 { 265 if ($this->mode != 'xhtml') return; 266 if (empty($this->data['dynfilters'])) return; 267 if (is_a($this->renderer, 'renderer_plugin_dw2pdf')) { 268 return; 269 } 270 global $conf; 271 272 $this->renderer->doc .= '<tr class="dataflt">'; 273 274 // add extra column for row numbers 275 if ($this->data['rownumbers']) { 276 $this->renderer->doc .= '<th></th>'; 277 } 278 279 // each column gets a form 280 foreach ($this->columns as $column) { 281 $this->renderer->doc .= '<th>'; 282 283 // BEGIN FORM 284 $form = new \Doku_Form(array('method' => 'GET', 'action' => wl($this->id))); 285 unset($form->_hidden['sectok']); // we don't need it here 286 if (!$conf['userewrite']) $form->addHidden('id', $this->id); 287 288 // current value 289 $dynamic = $this->searchConfig->getDynamicParameters(); 290 $filters = $dynamic->getFilters(); 291 if (isset($filters[$column->getFullQualifiedLabel()])) { 292 list(, $current) = $filters[$column->getFullQualifiedLabel()]; 293 $dynamic->removeFilter($column); 294 } else { 295 $current = ''; 296 } 297 298 // Add current request params 299 $params = $dynamic->getURLParameters(); 300 foreach ($params as $key => $val) { 301 $form->addHidden($key, $val); 302 } 303 304 // add input field 305 $key = $column->getFullQualifiedLabel() . $column->getType()->getDefaultComparator(); 306 $form->addElement( 307 form_makeField('text', SearchConfigParameters::$PARAM_FILTER . '[' . $key . ']', $current, '') 308 ); 309 $this->renderer->doc .= $form->getForm(); 310 // END FORM 311 312 $this->renderer->doc .= '</th>'; 313 } 314 $this->renderer->doc .= '</tr>'; 315 } 316 317 /** 318 * Display the actual table data 319 */ 320 protected function renderResult() 321 { 322 foreach ($this->result as $rownum => $row) { 323 $data = array( 324 'id' => $this->id, 325 'mode' => $this->mode, 326 'renderer' => $this->renderer, 327 'searchConfig' => $this->searchConfig, 328 'data' => $this->data, 329 'rownum' => &$rownum, 330 'row' => &$row, 331 ); 332 $evt = new \Doku_Event('PLUGIN_STRUCT_AGGREGATIONTABLE_RENDERRESULTROW', $data); 333 if ($evt->advise_before()) { 334 $this->renderResultRow($rownum, $row); 335 } 336 $evt->advise_after(); 337 } 338 } 339 340 /** 341 * Render a single result row 342 * 343 * @param int $rownum 344 * @param array $row 345 */ 346 protected function renderResultRow($rownum, $row) 347 { 348 $this->renderer->tablerow_open(); 349 350 // add data attribute for inline edit 351 if ($this->mode == 'xhtml') { 352 $pid = $this->resultPIDs[$rownum]; 353 $rid = $this->resultRids[$rownum]; 354 $rev = $this->resultRevs[$rownum]; 355 $this->renderer->doc = substr(rtrim($this->renderer->doc), 0, -1); // remove closing '>' 356 $this->renderer->doc .= ' data-pid="' . hsc($pid) . '" data-rev="' . $rev . '" data-rid="' . $rid . '">'; 357 } 358 359 // row number column 360 if (!empty($this->data['rownumbers'])) { 361 $this->renderer->tablecell_open(); 362 $searchConfigConf = $this->searchConfig->getConf(); 363 $this->renderer->cdata($rownum + $searchConfigConf['offset'] + 1); 364 $this->renderer->tablecell_close(); 365 } 366 367 /** @var Value $value */ 368 foreach ($row as $colnum => $value) { 369 $align = isset($this->data['align'][$colnum]) ? $this->data['align'][$colnum] : null; 370 $this->renderer->tablecell_open(1, $align); 371 $value->render($this->renderer, $this->mode); 372 $this->renderer->tablecell_close(); 373 374 // summarize 375 if (!empty($this->data['summarize']) && is_numeric($value->getValue())) { 376 if (!isset($this->sums[$colnum])) { 377 $this->sums[$colnum] = 0; 378 } 379 $this->sums[$colnum] += $value->getValue(); 380 } 381 } 382 $this->renderer->tablerow_close(); 383 } 384 385 /** 386 * Renders an information row for when no results were found 387 */ 388 protected function renderEmptyResult() 389 { 390 $this->renderer->tablerow_open(); 391 $this->renderer->tablecell_open(count($this->columns) + $this->data['rownumbers'], 'center'); 392 $this->renderer->cdata($this->helper->getLang('none')); 393 $this->renderer->tablecell_close(); 394 $this->renderer->tablerow_close(); 395 } 396 397 /** 398 * Add sums if wanted 399 */ 400 protected function renderSums() 401 { 402 if (empty($this->data['summarize'])) return; 403 404 $this->renderer->info['struct_table_meta'] = true; 405 if ($this->mode == 'xhtml') { 406 /** @noinspection PhpMethodParametersCountMismatchInspection */ 407 $this->renderer->tablerow_open('summarize'); 408 } else { 409 $this->renderer->tablerow_open(); 410 } 411 412 if ($this->data['rownumbers']) { 413 $this->renderer->tableheader_open(); 414 $this->renderer->tableheader_close(); 415 } 416 417 $len = count($this->columns); 418 for ($i = 0; $i < $len; $i++) { 419 $this->renderer->tableheader_open(1, $this->data['align'][$i]); 420 if (!empty($this->sums[$i])) { 421 $this->renderer->cdata('∑ '); 422 $this->columns[$i]->getType()->renderValue($this->sums[$i], $this->renderer, $this->mode); 423 } else { 424 if ($this->mode == 'xhtml') { 425 $this->renderer->doc .= ' '; 426 } 427 } 428 $this->renderer->tableheader_close(); 429 } 430 $this->renderer->tablerow_close(); 431 $this->renderer->info['struct_table_meta'] = false; 432 } 433 434 /** 435 * Adds paging controls to the table 436 */ 437 protected function renderPagingControls() 438 { 439 if (empty($this->data['limit'])) return; 440 if ($this->mode != 'xhtml') return; 441 442 $this->renderer->info['struct_table_meta'] = true; 443 $this->renderer->tablerow_open(); 444 $this->renderer->tableheader_open((count($this->columns) + ($this->data['rownumbers'] ? 1 : 0))); 445 $offset = $this->data['offset']; 446 447 // prev link 448 if ($offset) { 449 $prev = $offset - $this->data['limit']; 450 if ($prev < 0) { 451 $prev = 0; 452 } 453 454 $dynamic = $this->searchConfig->getDynamicParameters(); 455 $dynamic->setOffset($prev); 456 $link = wl($this->id, $dynamic->getURLParameters()); 457 $this->renderer->doc .= '<a href="' . $link . '" class="prev">' . $this->helper->getLang('prev') . '</a>'; 458 } 459 460 // next link 461 if ($this->resultCount > $offset + $this->data['limit']) { 462 $next = $offset + $this->data['limit']; 463 $dynamic = $this->searchConfig->getDynamicParameters(); 464 $dynamic->setOffset($next); 465 $link = wl($this->id, $dynamic->getURLParameters()); 466 $this->renderer->doc .= '<a href="' . $link . '" class="next">' . $this->helper->getLang('next') . '</a>'; 467 } 468 469 $this->renderer->tableheader_close(); 470 $this->renderer->tablerow_close(); 471 $this->renderer->info['struct_table_meta'] = true; 472 } 473 474 /** 475 * Adds CSV export controls 476 */ 477 protected function renderExportControls() 478 { 479 if ($this->mode != 'xhtml') return; 480 if (empty($this->data['csv'])) return; 481 if (!$this->resultCount) return; 482 483 $dynamic = $this->searchConfig->getDynamicParameters(); 484 $params = $dynamic->getURLParameters(); 485 $params['hash'] = $this->renderer->info['struct_table_hash']; 486 487 // FIXME apply dynamic filters 488 $link = exportlink($this->id, 'struct_csv', $params); 489 490 $this->renderer->doc .= '<a href="' . $link . '" class="export mediafile mf_csv">' . 491 $this->helper->getLang('csvexport') . '</a>'; 492 } 493} 494