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