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