xref: /plugin/struct/syntax/table.php (revision 53ed31251c00ac2e5b0c768ea5bae4cf4ab80c26)
1<?php
2/**
3 * DokuWiki Plugin struct (Syntax Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Andreas Gohr, Michael Große <dokuwiki@cosmocode.de>
7 */
8
9// must be run within Dokuwiki
10use plugin\struct\meta\Column;
11use plugin\struct\meta\ConfigParser;
12use plugin\struct\meta\SearchConfig;
13use plugin\struct\meta\StructException;
14
15if (!defined('DOKU_INC')) die();
16
17class syntax_plugin_struct_table extends DokuWiki_Syntax_Plugin {
18    /**
19     * @return string Syntax mode type
20     */
21    public function getType() {
22        return 'substition';
23    }
24    /**
25     * @return string Paragraph type
26     */
27    public function getPType() {
28        return 'block';
29    }
30    /**
31     * @return int Sort order - Low numbers go before high numbers
32     */
33    public function getSort() {
34        return 155;
35    }
36
37    /**
38     * Connect lookup pattern to lexer.
39     *
40     * @param string $mode Parser mode
41     */
42    public function connectTo($mode) {
43        $this->Lexer->addSpecialPattern('----+ *struct table *-+\n.*?\n----+', $mode, 'plugin_struct_table');
44    }
45
46
47    /**
48     * Handle matches of the struct syntax
49     *
50     * @param string $match The match of the syntax
51     * @param int    $state The state of the handler
52     * @param int    $pos The position in the document
53     * @param Doku_Handler    $handler The handler
54     * @return array Data for the renderer
55     */
56    public function handle($match, $state, $pos, Doku_Handler $handler){
57
58        $lines = explode("\n", $match);
59        array_shift($lines);
60        array_pop($lines);
61
62        try {
63            $parser = new ConfigParser($lines);
64            return  $parser->getConfig();
65        } catch (StructException $e) {
66            msg($e->getMessage(), -1, $e->getLine(), $e->getFile());
67            return null;
68        }
69    }
70
71    protected $sums = array();
72
73    /**
74     * Render xhtml output or metadata
75     *
76     * @param string         $mode      Renderer mode (supported modes: xhtml)
77     * @param Doku_Renderer  $renderer  The renderer
78     * @param array          $data      The data from the handler() function
79     * @return bool If rendering was successful.
80     */
81    public function render($mode, Doku_Renderer $renderer, $data) {
82        if(!$data) return false;
83
84        if($mode == 'metadata') {
85            /** @var Doku_Renderer_metadata $renderer  */
86            $renderer->meta['plugin']['struct']['hasaggregation'] = true;
87        }
88
89        //reset counters
90        $this->sums = array();
91
92        try {
93            $search = new SearchConfig($data);
94            $data = $search->getConf();
95            $rows = $search->execute();
96            $cnt = $search->getCount();
97            $cols = $search->getColumns();
98
99            if ($cnt === 0) {
100                $this->nullList($data, $mode, $renderer, $cols);
101                return true;
102            }
103
104            $this->renderPreTable($mode, $renderer, $data, $cols);
105            $this->renderRows($mode, $renderer, $data, $rows);
106            $this->renderPostTable($mode, $renderer, $data, $cnt);
107        } catch (StructException $e) {
108            msg($e->getMessage(), -1, $e->getLine(), $e->getFile());
109        }
110
111        return true;
112    }
113
114    /**
115     * create the pretext to the actual table rows
116     *
117     * @param string $mode
118     * @param Doku_Renderer $renderer
119     * @param array $data the configuration data
120     * @param Column[] $cols
121     */
122    protected function renderPreTable($mode, Doku_Renderer $renderer, $data, $cols) {
123        $this->startScope($mode, $renderer, md5(serialize($data)));
124        $this->showActiveFilters($mode, $renderer);
125        $this->startTable($mode, $renderer);
126        $renderer->tablethead_open();
127        $this->buildColumnHeaders($mode, $renderer, $data, $cols);
128        $this->addDynamicFilters($mode, $renderer, $data);
129        $renderer->tablethead_close();
130    }
131
132    /**
133     * @param string $mode current render mode
134     * @param Doku_Renderer $renderer
135     * @param array $data
136     * @param int $rowcnt
137     * @return string
138     */
139    private function renderPostTable($mode, Doku_Renderer $renderer, $data, $rowcnt) {
140        $this->summarize($mode, $renderer, $data, $this->sums);
141        $this->addLimitControls($mode, $renderer, $data, $rowcnt);
142        $this->finishTableAndScope($mode, $renderer);
143    }
144
145    /**
146     * if limit was set, add control
147     *
148     * @param string        $mode     the mode of the renderer
149     * @param Doku_Renderer $renderer the renderer
150     * @param array         $data     the configuration of the table/search
151     * @param               $rowcnt
152     */
153    protected function addLimitControls($mode, Doku_Renderer $renderer, $data, $rowcnt) {
154        global $ID;
155
156        if($data['limit']) {
157            $renderer->tablerow_open();
158            $renderer->tableheader_open((count($data['cols']) + ($data['rownumbers'] ? 1 : 0)));
159            $offset = (int) $_REQUEST['dataofs'];
160
161            // keep url params
162            $params = array();
163            if (!empty($data['current_params']['dataflt'])) {$params['dataflt'] = $data['current_params']['dataflt'];}
164            if (!empty($data['current_params']['datasrt'])) {$params['datasrt'] = $data['current_params']['datasrt'];}
165
166            if($offset) {
167                $prev = $offset - $data['limit'];
168                if($prev < 0) {
169                    $prev = 0;
170                }
171                $params['dataofs'] = $prev;
172                $renderer->internallink($ID . '?' . http_build_query($params), $this->getLang('prev'));
173            }
174
175            if($rowcnt > $offset + $data['limit']) {
176                $next = $offset + $data['limit'];
177                $params['dataofs'] = $next;
178                $renderer->internallink($ID . '?' . http_build_query($params), $this->getLang('next'));
179            }
180            $renderer->tableheader_close();
181            $renderer->tablerow_close();
182        }
183    }
184
185    /**
186     * @param string        $mode     the mode of the renderer
187     * @param Doku_Renderer $renderer the renderer
188     */
189    protected function showActiveFilters($mode, Doku_Renderer $renderer) {
190        global $ID;
191
192        if($mode == 'xhtml' && !empty($data['current_params']['dataflt'])) {
193            $filters = $data['current_params']['dataflt'];
194            /** @var helper_plugin_struct_config $confHelper */
195            $confHelper = $this->loadHelper('struct_config');
196            $fltrs = array();
197            foreach($filters as $colcomp => $filter) {
198                $filter = $confHelper->parseFilterLine('', $colcomp.$filter);
199                if(strpos($filter[1], '~') !== false) {
200                    if(strpos($filter[1], '!~') !== false) {
201                        $comparator_value = '!~' . str_replace('%', '*', $filter[2]);
202                    } else {
203                        $comparator_value = '~' . str_replace('%', '', $filter[2]);
204                    }
205                    $fltrs[] = $filter[0] . $comparator_value;
206                } else {
207                    $fltrs[] = $filter[0] . $filter[1] . $filter[2];
208                }
209            }
210
211            $renderer->doc .= '<div class="filter">';
212            $renderer->doc .= '<h4>' . sprintf($this->getLang('tablefilteredby'), hsc(implode(' & ', $fltrs))) . '</h4>';
213            $renderer->doc .= '<div class="resetfilter">';
214            $renderer->internallink($ID, $this->getLang('tableresetfilter'));
215            $renderer->doc .=  '</div>';
216            $renderer->doc .= '</div>';
217        }
218    }
219
220    /**
221     * @param string        $mode     the mode of the renderer
222     * @param Doku_Renderer $renderer the renderer
223     * @param array         $data     the configuration of the table/search
224     */
225    protected function addDynamicFilters($mode, Doku_Renderer $renderer, $data) {
226        if ($mode != 'xhtml') return;
227
228        global $conf, $ID;
229
230        $cur_params = $data['current_params'];
231        $html = '';
232        if($data['dynfilters']) {
233            $html .= '<tr class="dataflt">';
234
235            if($data['rownumbers']) {
236                $html .= '<th></th>';
237            }
238
239            foreach($data['headers'] as $num => $head) {
240                $html .= '<th>';
241                $form = new Doku_Form(array('method' => 'GET',));
242                $form->_hidden = array();
243                if(!$conf['userewrite']) {
244                    $form->addHidden('id', $ID);
245                }
246
247                $key = $data['cols'][$num] . '*~';
248                $val = isset($cur_params['dataflt'][$key]) ? $cur_params['dataflt'][$key] : '';
249
250                // Add current request params
251                if (!empty($cur_params['datasrt'])) {
252                    $form->addHidden('datasrt', $cur_params['datasrt']);
253                }
254                if (!empty($cur_params['dataofs'])) {
255                    $form->addHidden('dataofs', $cur_params['dataofs']);
256                }
257                if (!empty($cur_params['dataflt'])) foreach($cur_params['dataflt'] as $c_key => $c_val) {
258                    if($c_val !== '' && $c_key !== $key) {
259                        $form->addHidden('dataflt[' . $c_key . ']', $c_val);
260                    }
261                }
262
263                $form->addElement(form_makeField('text', 'dataflt[' . $key . ']', $val, ''));
264                $html .= $form->getForm();
265                $html .= '</th>';
266            }
267            $html .= '</tr>';
268            $renderer->doc .= $html;
269        }
270    }
271
272    /**
273     * @param string        $mode     the mode of the renderer
274     * @param Doku_Renderer $renderer the renderer
275     */
276    private function startTable($mode, Doku_Renderer $renderer) {
277        $renderer->table_open();
278    }
279
280    /**
281     * @param string $mode the mode of the renderer
282     * @param Doku_Renderer $renderer the renderer
283     * @param array $data the configuration of the table/search
284     * @param Column[] $cols
285     */
286    protected function buildColumnHeaders($mode, Doku_Renderer $renderer, $data, $cols) {
287        global $ID;
288
289        $renderer->tablerow_open();
290
291        if($data['rownumbers']) {
292            $renderer->tableheader_open();
293            $renderer->cdata('#');
294            $renderer->tableheader_close();
295        }
296
297        foreach($data['headers'] as $num => $head) {
298            $ckey = $data['cols'][$num];
299            if(blank($head)) {
300                if(isset($cols[$num]) && is_a($cols[$num], 'plugin\struct\meta\PageColumn')) {
301                    $head = $this->getLang('pagelabel');
302                }else if(isset($cols[$num]) && is_a($cols[$num], 'plugin\struct\meta\Column')) {
303                    $head = $cols[$num]->getTranslatedLabel();
304                } else {
305                    $head = 'column '.$num; // this should never happen
306                }
307            }
308
309            $width = '';
310            if(isset($data['widths'][$num]) AND $data['widths'][$num] != '-') {
311                $width = ' style="width: ' . $data['widths'][$num] . ';"';
312            }
313            if ($mode == 'xhmtl') {
314                $renderer->doc .= '<th' . $width . '>';
315            } else {
316                $renderer->tableheader_open();
317            }
318
319            // output header
320            if ($mode == 'xhtml') {
321                $sort = '';
322                if(isset($data['sort']) && $ckey == $data['sort'][0]) {
323                    if($data['sort'][1] == 'ASC') {
324                        $sort = 'sort-down';
325                        $ckey = '^' . $ckey;
326                    } else {
327                        $sort = 'sort-up';
328                    }
329                }
330
331                $params = $data['current_params'];
332                $params['datasrt'] = $ckey;
333                $link = wl($ID, $params);
334                $renderer->doc .= '<a href="'.$link.'" class="'.$sort.'" title="'.$this->getLang('sort').'">'.hsc($head).'</a>';
335            } else {
336                $renderer->cdata($head);
337            }
338
339            $renderer->tableheader_close();
340        }
341        $renderer->tablerow_close();
342    }
343
344    /**
345     * @param string        $mode     the mode of the renderer
346     * @param Doku_Renderer $renderer the renderer
347     * @param string        $hash     hash to identify the table and group images in gallery
348     */
349    protected function startScope($mode, \Doku_Renderer $renderer, $hash) {
350        if ($mode == 'xhtml') {
351            $renderer->doc .= "<div class=\"structaggregation\">";
352            $renderer->info['struct_table_hash'] = $hash;
353        }
354    }
355
356    /**
357     * if summarize was set, add sums
358     *
359     * @param string        $mode     the mode of the renderer
360     * @param Doku_Renderer $renderer the renderer
361     * @param array         $data     the configuration of the table/search
362     * @param array         $sums     the summarized output of the numerical fields
363     */
364    private function summarize($mode, \Doku_Renderer $renderer, $data, $sums) {
365        if($data['summarize']) {
366            $renderer->tablerow_open();
367            $len = count($data['cols']);
368
369            if($data['rownumbers']) {
370                $renderer->tablecell_open();
371                $renderer->tablecell_close();
372            }
373
374            for($i = 0; $i < $len; $i++) {
375                $renderer->tablecell_open(1, $data['align'][$i]);
376                if(!empty($sums[$i])) {
377                    $renderer->cdata('∑ ' . $sums[$i]);
378                } else {
379                    if ($mode == 'xhtml') {
380                        $renderer->doc .= '&nbsp;';
381                    }
382                }
383                $renderer->tablecell_close();
384            }
385            $renderer->tablerow_close();
386        }
387    }
388
389    /**
390     * @param string        $mode     the mode of the renderer
391     * @param Doku_Renderer $renderer the renderer
392     *
393     */
394    private function finishTableAndScope($mode, Doku_Renderer $renderer) {
395        $renderer->table_close();
396        if ($mode == 'xhtml') {
397            $renderer->doc .= '</div>';
398            if(isset($renderer->info['struct_table_hash'])) {
399                unset($renderer->info['struct_table_hash']);
400            }
401        }
402    }
403
404    /**
405     * @param string        $mode     the mode of the renderer
406     * @param Doku_Renderer $renderer the renderer
407     * @param array         $data     the configuration of the table/search
408     * @param               $rows
409     *
410     */
411    private function renderRows($mode, Doku_Renderer $renderer, $data, $rows) {
412        $renderer->tabletbody_open();
413        foreach($rows as $rownum => $row) {
414            $renderer->tablerow_open();
415
416            if($data['rownumbers']) {
417                $renderer->tablecell_open();
418                $renderer->doc .= $rownum + 1;
419                $renderer->tablecell_close();
420            }
421
422            /** @var plugin\struct\meta\Value $value */
423            foreach($row as $colnum => $value) {
424                $renderer->tablecell_open();
425                $value->render($renderer, $mode);
426                $renderer->tablecell_close();
427
428                // summarize
429                if($data['summarize'] && is_numeric($value->getValue())) {
430                    if(!isset($this->sums[$colnum])) {
431                        $this->sums[$colnum] = 0;
432                    }
433                    $this->sums[$colnum] += $value->getValue();
434                }
435            }
436            $renderer->tablerow_close();
437        }
438        $renderer->tabletbody_close();
439    }
440
441    /**
442     * @param array $data the configuration of the table/search
443     * @param string $mode the mode of the renderer
444     * @param Doku_Renderer $renderer the renderer
445     * @param Column[] $cols
446     */
447    private function nullList($data, $mode, Doku_Renderer $renderer, $cols) {
448        $this->renderPreTable($mode, $renderer, $data, $cols);
449        $renderer->tablerow_open();
450        $renderer->tablecell_open(count($data['cols']) + $data['rownumbers'], 'center');
451        $renderer->cdata($this->getLang('none'));
452        $renderer->tablecell_close();
453        $renderer->tablerow_close();
454        $renderer->table_close();
455    }
456}
457
458// vim:ts=4:sw=4:et:
459