xref: /plugin/struct/meta/AggregationTable.php (revision 7234bfb14e712ff548d9266ef32fdcc8eaf2d04e)
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 .= '&nbsp;';
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