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