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->renderer->cdata($this->helper->getLang('none')); 35 return; 36 } 37 38 $this->renderActiveFilters(); 39 40 $rendercontext = array( 41 'table' => $this, 42 'renderer' => $this->renderer, 43 'format' => $this->mode, 44 'search' => $this->searchConfig, 45 'columns' => $this->columns, 46 'data' => $this->result 47 ); 48 49 $event = new \Doku_Event( 50 'PLUGIN_STRUCT_RENDER_AGGREGATION_TABLE', 51 $rendercontext 52 ); 53 $event->trigger([$this, 'renderTable']); 54 55 // export handle 56 $this->renderExportControls(); 57 } 58 59 /** 60 * Render the default aggregation table 61 */ 62 public function renderTable($rendercontext) 63 { 64 $this->renderer->table_open(); 65 66 // header 67 $this->renderer->tablethead_open(); 68 $this->renderColumnHeaders(); 69 $this->renderDynamicFilters(); 70 $this->renderer->tablethead_close(); 71 72 if ($this->resultCount) { 73 // actual data 74 $this->renderer->tabletbody_open(); 75 $this->renderResult(); 76 $this->renderer->tabletbody_close(); 77 78 // footer (tfoot is develonly currently) 79 if (method_exists($this->renderer, 'tabletfoot_open')) $this->renderer->tabletfoot_open(); 80 $this->renderSums(); 81 $this->renderPagingControls(); 82 if (method_exists($this->renderer, 'tabletfoot_close')) $this->renderer->tabletfoot_close(); 83 } else { 84 // nothing found 85 $this->renderEmptyResult(); 86 } 87 88 // table close 89 $this->renderer->table_close(); 90 } 91 92 /** 93 * Adds additional info to document and renderer in XHTML mode 94 * 95 * @see finishScope() 96 */ 97 public function startScope() 98 { 99 // unique identifier for this aggregation 100 $this->renderer->info['struct_table_hash'] = md5(var_export($this->data, true)); 101 102 parent::startScope(); 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 = array(); 131 foreach ($filters as $column => $filter) { 132 list($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 list(/*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 $this->renderer->doc .= '<a href="' . $link . '" class="' . $sortclass . '" ' . 232 'title="' . $this->helper->getLang('sort') . '">' . hsc($header) . '</a>'; 233 $this->renderer->doc .= '</th>'; 234 } 235 236 $this->renderer->tablerow_close(); 237 } 238 239 /** 240 * Is the result set currently dynamically filtered? 241 * @return bool 242 */ 243 protected function isDynamicallyFiltered() 244 { 245 if ($this->mode != 'xhtml') return false; 246 if (!$this->data['dynfilters']) return false; 247 248 $dynamic = $this->searchConfig->getDynamicParameters(); 249 return (bool)$dynamic->getFilters(); 250 } 251 252 /** 253 * Add input fields for dynamic filtering 254 */ 255 protected function renderDynamicFilters() 256 { 257 if ($this->mode != 'xhtml') return; 258 if (empty($this->data['dynfilters'])) return; 259 if (is_a($this->renderer, 'renderer_plugin_dw2pdf')) { 260 return; 261 } 262 global $conf; 263 264 $this->renderer->doc .= '<tr class="dataflt">'; 265 266 // add extra column for row numbers 267 if ($this->data['rownumbers']) { 268 $this->renderer->doc .= '<th></th>'; 269 } 270 271 // each column gets a form 272 foreach ($this->columns as $column) { 273 $this->renderer->doc .= '<th>'; 274 275 // BEGIN FORM 276 $form = new \Doku_Form(array('method' => 'GET', 'action' => wl($this->id))); 277 unset($form->_hidden['sectok']); // we don't need it here 278 if (!$conf['userewrite']) $form->addHidden('id', $this->id); 279 280 // current value 281 $dynamic = $this->searchConfig->getDynamicParameters(); 282 $filters = $dynamic->getFilters(); 283 if (isset($filters[$column->getFullQualifiedLabel()])) { 284 list(, $current) = $filters[$column->getFullQualifiedLabel()]; 285 $dynamic->removeFilter($column); 286 } else { 287 $current = ''; 288 } 289 290 // Add current request params 291 $params = $dynamic->getURLParameters(); 292 foreach ($params as $key => $val) { 293 $form->addHidden($key, $val); 294 } 295 296 // add input field 297 $key = $column->getFullQualifiedLabel() . $column->getType()->getDefaultComparator(); 298 $form->addElement( 299 form_makeField('text', SearchConfigParameters::$PARAM_FILTER . '[' . $key . ']', $current, '') 300 ); 301 $this->renderer->doc .= $form->getForm(); 302 // END FORM 303 304 $this->renderer->doc .= '</th>'; 305 } 306 $this->renderer->doc .= '</tr>'; 307 } 308 309 /** 310 * Display the actual table data 311 */ 312 protected function renderResult() 313 { 314 foreach ($this->result as $rownum => $row) { 315 $data = array( 316 'id' => $this->id, 317 'mode' => $this->mode, 318 'renderer' => $this->renderer, 319 'searchConfig' => $this->searchConfig, 320 'data' => $this->data, 321 'rownum' => &$rownum, 322 'row' => &$row, 323 ); 324 $evt = new \Doku_Event('PLUGIN_STRUCT_AGGREGATIONTABLE_RENDERRESULTROW', $data); 325 if ($evt->advise_before()) { 326 $this->renderResultRow($rownum, $row); 327 } 328 $evt->advise_after(); 329 } 330 } 331 332 /** 333 * Render a single result row 334 * 335 * @param int $rownum 336 * @param array $row 337 */ 338 protected function renderResultRow($rownum, $row) 339 { 340 $this->renderer->tablerow_open(); 341 342 // add data attribute for inline edit 343 if ($this->mode == 'xhtml') { 344 $pid = $this->resultPIDs[$rownum]; 345 $rid = $this->resultRids[$rownum]; 346 $rev = $this->resultRevs[$rownum]; 347 $this->renderer->doc = substr(rtrim($this->renderer->doc), 0, -1); // remove closing '>' 348 $this->renderer->doc .= ' data-pid="' . hsc($pid) . '" data-rev="' . $rev . '" data-rid="' . $rid . '">'; 349 } 350 351 // row number column 352 if (!empty($this->data['rownumbers'])) { 353 $this->renderer->tablecell_open(); 354 $searchConfigConf = $this->searchConfig->getConf(); 355 $this->renderer->cdata($rownum + $searchConfigConf['offset'] + 1); 356 $this->renderer->tablecell_close(); 357 } 358 359 /** @var Value $value */ 360 foreach ($row as $colnum => $value) { 361 $align = isset($this->data['align'][$colnum]) ? $this->data['align'][$colnum] : null; 362 $this->renderer->tablecell_open(1, $align); 363 $value->render($this->renderer, $this->mode); 364 $this->renderer->tablecell_close(); 365 366 // summarize 367 if (!empty($this->data['summarize']) && is_numeric($value->getValue())) { 368 if (!isset($this->sums[$colnum])) { 369 $this->sums[$colnum] = 0; 370 } 371 $this->sums[$colnum] += $value->getValue(); 372 } 373 } 374 $this->renderer->tablerow_close(); 375 } 376 377 /** 378 * Renders an information row for when no results were found 379 */ 380 protected function renderEmptyResult() 381 { 382 $this->renderer->tablerow_open(); 383 $this->renderer->tablecell_open(count($this->columns) + $this->data['rownumbers'], 'center'); 384 $this->renderer->cdata($this->helper->getLang('none')); 385 $this->renderer->tablecell_close(); 386 $this->renderer->tablerow_close(); 387 } 388 389 /** 390 * Add sums if wanted 391 */ 392 protected function renderSums() 393 { 394 if (empty($this->data['summarize'])) return; 395 396 $this->renderer->info['struct_table_meta'] = true; 397 if ($this->mode == 'xhtml') { 398 /** @noinspection PhpMethodParametersCountMismatchInspection */ 399 $this->renderer->tablerow_open('summarize'); 400 } else { 401 $this->renderer->tablerow_open(); 402 } 403 404 if ($this->data['rownumbers']) { 405 $this->renderer->tableheader_open(); 406 $this->renderer->tableheader_close(); 407 } 408 409 $len = count($this->columns); 410 for ($i = 0; $i < $len; $i++) { 411 $this->renderer->tableheader_open(1, $this->data['align'][$i]); 412 if (!empty($this->sums[$i])) { 413 $this->renderer->cdata('∑ '); 414 $this->columns[$i]->getType()->renderValue($this->sums[$i], $this->renderer, $this->mode); 415 } else { 416 if ($this->mode == 'xhtml') { 417 $this->renderer->doc .= ' '; 418 } 419 } 420 $this->renderer->tableheader_close(); 421 } 422 $this->renderer->tablerow_close(); 423 $this->renderer->info['struct_table_meta'] = false; 424 } 425 426 /** 427 * Adds paging controls to the table 428 */ 429 protected function renderPagingControls() 430 { 431 if (empty($this->data['limit'])) return; 432 if ($this->mode != 'xhtml') return; 433 434 $this->renderer->info['struct_table_meta'] = true; 435 $this->renderer->tablerow_open(); 436 $this->renderer->tableheader_open((count($this->columns) + ($this->data['rownumbers'] ? 1 : 0))); 437 $offset = $this->data['offset']; 438 439 // prev link 440 if ($offset) { 441 $prev = $offset - $this->data['limit']; 442 if ($prev < 0) { 443 $prev = 0; 444 } 445 446 $dynamic = $this->searchConfig->getDynamicParameters(); 447 $dynamic->setOffset($prev); 448 $link = wl($this->id, $dynamic->getURLParameters()); 449 $this->renderer->doc .= '<a href="' . $link . '" class="prev">' . $this->helper->getLang('prev') . '</a>'; 450 } 451 452 // next link 453 if ($this->resultCount > $offset + $this->data['limit']) { 454 $next = $offset + $this->data['limit']; 455 $dynamic = $this->searchConfig->getDynamicParameters(); 456 $dynamic->setOffset($next); 457 $link = wl($this->id, $dynamic->getURLParameters()); 458 $this->renderer->doc .= '<a href="' . $link . '" class="next">' . $this->helper->getLang('next') . '</a>'; 459 } 460 461 $this->renderer->tableheader_close(); 462 $this->renderer->tablerow_close(); 463 $this->renderer->info['struct_table_meta'] = true; 464 } 465 466 /** 467 * Adds CSV export controls 468 */ 469 protected function renderExportControls() 470 { 471 if ($this->mode != 'xhtml') return; 472 if (empty($this->data['csv'])) return; 473 if (!$this->resultCount) return; 474 475 $dynamic = $this->searchConfig->getDynamicParameters(); 476 $params = $dynamic->getURLParameters(); 477 $params['hash'] = $this->renderer->info['struct_table_hash']; 478 479 // FIXME apply dynamic filters 480 $link = exportlink($this->id, 'struct_csv', $params); 481 482 $this->renderer->doc .= '<a href="' . $link . '" class="export mediafile mf_csv">' . 483 $this->helper->getLang('csvexport') . '</a>'; 484 } 485} 486