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