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 = ['table' => $this, 'renderer' => $this->renderer, 'format' => $this->mode, 'search' => $this->searchConfig, 'columns' => $this->columns, 'data' => $this->result]; 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->resultCount) { 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 [, $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 $this->renderer->doc .= '<a href="' . $link . '" class="' . $sortclass . '" ' . 227 'title="' . $this->helper->getLang('sort') . '">' . hsc($header) . '</a>'; 228 $this->renderer->doc .= '</th>'; 229 } 230 231 $this->renderer->tablerow_close(); 232 } 233 234 /** 235 * Is the result set currently dynamically filtered? 236 * @return bool 237 */ 238 protected function isDynamicallyFiltered() 239 { 240 if ($this->mode != 'xhtml') return false; 241 if (!$this->data['dynfilters']) return false; 242 243 $dynamic = $this->searchConfig->getDynamicParameters(); 244 return (bool)$dynamic->getFilters(); 245 } 246 247 /** 248 * Add input fields for dynamic filtering 249 */ 250 protected function renderDynamicFilters() 251 { 252 if ($this->mode != 'xhtml') return; 253 if (empty($this->data['dynfilters'])) return; 254 if (is_a($this->renderer, 'renderer_plugin_dw2pdf')) { 255 return; 256 } 257 global $conf; 258 259 $this->renderer->doc .= '<tr class="dataflt">'; 260 261 // add extra column for row numbers 262 if ($this->data['rownumbers']) { 263 $this->renderer->doc .= '<th></th>'; 264 } 265 266 // each column gets a form 267 foreach ($this->columns as $column) { 268 $this->renderer->doc .= '<th>'; 269 270 // BEGIN FORM 271 $form = new \Doku_Form(['method' => 'GET', 'action' => wl($this->id)]); 272 unset($form->_hidden['sectok']); // we don't need it here 273 if (!$conf['userewrite']) $form->addHidden('id', $this->id); 274 275 // current value 276 $dynamic = $this->searchConfig->getDynamicParameters(); 277 $filters = $dynamic->getFilters(); 278 if (isset($filters[$column->getFullQualifiedLabel()])) { 279 [, $current] = $filters[$column->getFullQualifiedLabel()]; 280 $dynamic->removeFilter($column); 281 } else { 282 $current = ''; 283 } 284 285 // Add current request params 286 $params = $dynamic->getURLParameters(); 287 foreach ($params as $key => $val) { 288 $form->addHidden($key, $val); 289 } 290 291 // add input field 292 $key = $column->getFullQualifiedLabel() . $column->getType()->getDefaultComparator(); 293 $form->addElement( 294 form_makeField('text', SearchConfigParameters::$PARAM_FILTER . '[' . $key . ']', $current, '') 295 ); 296 $this->renderer->doc .= $form->getForm(); 297 // END FORM 298 299 $this->renderer->doc .= '</th>'; 300 } 301 $this->renderer->doc .= '</tr>'; 302 } 303 304 /** 305 * Display the actual table data 306 */ 307 protected function renderResult() 308 { 309 foreach ($this->result as $rownum => $row) { 310 $data = ['id' => $this->id, 'mode' => $this->mode, 'renderer' => $this->renderer, 'searchConfig' => $this->searchConfig, 'data' => $this->data, 'rownum' => &$rownum, 'row' => &$row]; 311 $evt = new Event('PLUGIN_STRUCT_AGGREGATIONTABLE_RENDERRESULTROW', $data); 312 if ($evt->advise_before()) { 313 $this->renderResultRow($rownum, $row); 314 } 315 $evt->advise_after(); 316 } 317 } 318 319 /** 320 * Render a single result row 321 * 322 * @param int $rownum 323 * @param array $row 324 */ 325 protected function renderResultRow($rownum, $row) 326 { 327 $this->renderer->tablerow_open(); 328 329 // add data attribute for inline edit 330 if ($this->mode == 'xhtml') { 331 $pid = $this->resultPIDs[$rownum]; 332 $rid = $this->resultRids[$rownum]; 333 $rev = $this->resultRevs[$rownum]; 334 $this->renderer->doc = substr(rtrim($this->renderer->doc), 0, -1); // remove closing '>' 335 $this->renderer->doc .= ' data-pid="' . hsc($pid) . '" data-rev="' . $rev . '" data-rid="' . $rid . '">'; 336 } 337 338 // row number column 339 if (!empty($this->data['rownumbers'])) { 340 $this->renderer->tablecell_open(); 341 $this->renderer->cdata($rownum + $this->searchConfig->getOffset() + 1); 342 $this->renderer->tablecell_close(); 343 } 344 345 /** @var Value $value */ 346 foreach ($row as $colnum => $value) { 347 $align = $this->data['align'][$colnum] ?? null; 348 $this->renderer->tablecell_open(1, $align); 349 $value->render($this->renderer, $this->mode); 350 $this->renderer->tablecell_close(); 351 352 // summarize 353 if (!empty($this->data['summarize']) && is_numeric($value->getValue())) { 354 if (!isset($this->sums[$colnum])) { 355 $this->sums[$colnum] = 0; 356 } 357 $this->sums[$colnum] += $value->getValue(); 358 } 359 } 360 $this->renderer->tablerow_close(); 361 } 362 363 /** 364 * Renders an information row for when no results were found 365 */ 366 protected function renderEmptyResult() 367 { 368 $this->renderer->tablerow_open(); 369 $this->renderer->tablecell_open(count($this->columns) + $this->data['rownumbers'], 'center'); 370 $this->renderer->cdata($this->helper->getLang('none')); 371 $this->renderer->tablecell_close(); 372 $this->renderer->tablerow_close(); 373 } 374 375 /** 376 * Add sums if wanted 377 */ 378 protected function renderSums() 379 { 380 if (empty($this->data['summarize'])) return; 381 382 $this->renderer->info['struct_table_meta'] = true; 383 if ($this->mode == 'xhtml') { 384 /** @noinspection PhpMethodParametersCountMismatchInspection */ 385 $this->renderer->tablerow_open('summarize'); 386 } else { 387 $this->renderer->tablerow_open(); 388 } 389 390 if ($this->data['rownumbers']) { 391 $this->renderer->tableheader_open(); 392 $this->renderer->tableheader_close(); 393 } 394 395 $len = count($this->columns); 396 for ($i = 0; $i < $len; $i++) { 397 $this->renderer->tableheader_open(1, $this->data['align'][$i]); 398 if (!empty($this->sums[$i])) { 399 $this->renderer->cdata('∑ '); 400 $this->columns[$i]->getType()->renderValue($this->sums[$i], $this->renderer, $this->mode); 401 } elseif ($this->mode == 'xhtml') { 402 $this->renderer->doc .= ' '; 403 } 404 $this->renderer->tableheader_close(); 405 } 406 $this->renderer->tablerow_close(); 407 $this->renderer->info['struct_table_meta'] = false; 408 } 409 410 /** 411 * Adds paging controls to the table 412 */ 413 protected function renderPagingControls() 414 { 415 if ($this->mode != 'xhtml') return; 416 417 $limit = $this->searchConfig->getLimit(); 418 if (!$limit) return; 419 $offset = $this->searchConfig->getOffset(); 420 421 $this->renderer->info['struct_table_meta'] = true; 422 $this->renderer->tablerow_open(); 423 $this->renderer->tableheader_open((count($this->columns) + ($this->data['rownumbers'] ? 1 : 0))); 424 425 426 // prev link 427 if ($offset) { 428 $prev = $offset - $limit; 429 if ($prev < 0) { 430 $prev = 0; 431 } 432 433 $dynamic = $this->searchConfig->getDynamicParameters(); 434 $dynamic->setOffset($prev); 435 $link = wl($this->id, $dynamic->getURLParameters()); 436 $this->renderer->doc .= '<a href="' . $link . '" class="prev">' . $this->helper->getLang('prev') . '</a>'; 437 } 438 439 // next link 440 if ($this->resultCount > $offset + $limit) { 441 $next = $offset + $limit; 442 $dynamic = $this->searchConfig->getDynamicParameters(); 443 $dynamic->setOffset($next); 444 $link = wl($this->id, $dynamic->getURLParameters()); 445 $this->renderer->doc .= '<a href="' . $link . '" class="next">' . $this->helper->getLang('next') . '</a>'; 446 } 447 448 $this->renderer->tableheader_close(); 449 $this->renderer->tablerow_close(); 450 $this->renderer->info['struct_table_meta'] = true; 451 } 452 453 /** 454 * Adds CSV export controls 455 */ 456 protected function renderExportControls() 457 { 458 if ($this->mode != 'xhtml') return; 459 if (empty($this->data['csv'])) return; 460 if (!$this->resultCount) return; 461 462 $dynamic = $this->searchConfig->getDynamicParameters(); 463 $params = $dynamic->getURLParameters(); 464 $params['hash'] = $this->renderer->info['struct_table_hash']; 465 466 // FIXME apply dynamic filters 467 $link = exportlink($this->id, 'struct_csv', $params); 468 469 $this->renderer->doc .= '<a href="' . $link . '" class="export mediafile mf_csv">' . 470 $this->helper->getLang('csvexport') . '</a>'; 471 } 472} 473