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