1
|
|
|
<?php |
2
|
|
|
namespace Fwlib\Html; |
3
|
|
|
|
4
|
|
|
use Fwlib\Bridge\Adodb; |
5
|
|
|
use Fwlib\Bridge\Smarty; |
6
|
|
|
use Fwlib\Util\UtilContainerAwareTrait; |
7
|
|
|
|
8
|
|
|
/** |
9
|
|
|
* Html generator: list table |
10
|
|
|
* |
11
|
|
|
* Give table head, data and other necessary information, generate table html. |
12
|
|
|
* |
13
|
|
|
* Some html/style operation use jQuery. |
14
|
|
|
* |
15
|
|
|
* @codeCoverageIgnore |
16
|
|
|
* @deprecated Use ListView instead |
17
|
|
|
* |
18
|
|
|
* @copyright Copyright 2003-2015 Fwolf |
19
|
|
|
* @license http://www.gnu.org/licenses/lgpl.html LGPL-3.0+ |
20
|
|
|
*/ |
21
|
|
|
class ListTable |
22
|
|
|
{ |
23
|
|
|
use UtilContainerAwareTrait; |
24
|
|
|
|
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* If column in data and title does not match, fitMode option will |
28
|
|
|
* determine which columns will be used, its value defines here. |
29
|
|
|
*/ |
30
|
|
|
// Fit to title, drop data whose index is not in title index |
31
|
|
|
const FIT_TO_TITLE = 0; |
32
|
|
|
|
33
|
|
|
// Fit to data, drop title whose index is not in data index |
34
|
|
|
const FIT_TO_DATA = 1; |
35
|
|
|
|
36
|
|
|
// Fit to intersection of title and data, got fewest column |
37
|
|
|
const FIT_INTERSECTION = 2; |
38
|
|
|
|
39
|
|
|
// Fit to union of title and data, got most column |
40
|
|
|
const FIT_UNION = 3; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* Config array |
44
|
|
|
* |
45
|
|
|
* @var array |
46
|
|
|
*/ |
47
|
|
|
protected $configs = []; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* Information generated in treatment |
51
|
|
|
* |
52
|
|
|
* Default value is given according to default config. |
53
|
|
|
* |
54
|
|
|
* @var array |
55
|
|
|
*/ |
56
|
|
|
protected $info = [ |
57
|
|
|
// Class for root element |
58
|
|
|
'class' => 'ListTable', |
59
|
|
|
// Class prefix for non-root elements |
60
|
|
|
'classPrefix' => 'ListTable-', |
61
|
|
|
// Id of root element |
62
|
|
|
'id' => 'ListTable-1', |
63
|
|
|
// Id prefix for non-root elements |
64
|
|
|
'idPrefix' => 'ListTable-1-', |
65
|
|
|
// Orderby enabled column and default direction |
66
|
|
|
'orderByColumn' => [], |
67
|
|
|
// Current page number |
68
|
|
|
'page' => 1, |
69
|
|
|
// Max page number |
70
|
|
|
'pageMax' => 1, |
71
|
|
|
// Parsed pager text |
72
|
|
|
'pagerTextBody' => '', |
73
|
|
|
'totalRows' => -1, |
74
|
|
|
]; |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* List data array |
78
|
|
|
* |
79
|
|
|
* @var array |
80
|
|
|
*/ |
81
|
|
|
protected $listData = []; |
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* List title array, show as table title |
85
|
|
|
* |
86
|
|
|
* @var array |
87
|
|
|
*/ |
88
|
|
|
protected $listTitle = []; |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Page url param array |
92
|
|
|
* |
93
|
|
|
* @var array |
94
|
|
|
*/ |
95
|
|
|
protected $param = []; |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* Template object |
99
|
|
|
* |
100
|
|
|
* @var Smarty |
101
|
|
|
*/ |
102
|
|
|
protected $tpl = null; |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* Prefix of var assigned to template |
106
|
|
|
* |
107
|
|
|
* Should keep sync with var name in template file, in most case this |
108
|
|
|
* property need not change. |
109
|
|
|
* |
110
|
|
|
* @var string |
111
|
|
|
*/ |
112
|
|
|
protected $tplVarPrefix = 'listTable'; |
113
|
|
|
|
114
|
|
|
/** |
115
|
|
|
* Array of url, computed for show in tpl |
116
|
|
|
* |
117
|
|
|
* { |
118
|
|
|
* base : Original page url |
119
|
|
|
* form : Page jump form target url |
120
|
|
|
* obCur : Link on current orderBy head, to reverse order |
121
|
|
|
* obOther : Link on in-active orderBy head(their default dir add in tpl) |
122
|
|
|
* pageFirst : First page link |
123
|
|
|
* pageLast : Last page link |
124
|
|
|
* pageNext : Next page link |
125
|
|
|
* pagePrev : Prev page link |
126
|
|
|
* } |
127
|
|
|
* |
128
|
|
|
* @var array |
129
|
|
|
*/ |
130
|
|
|
protected $url = [ |
131
|
|
|
'base' => '', |
132
|
|
|
'form' => '', |
133
|
|
|
'obCur' => '', |
134
|
|
|
'obOther' => '', |
135
|
|
|
'pageFirst' => '', |
136
|
|
|
'pageLast' => '', |
137
|
|
|
'pageNext' => '', |
138
|
|
|
'pagePrev' => '', |
139
|
|
|
]; |
140
|
|
|
|
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* Constructor |
144
|
|
|
* |
145
|
|
|
* If there are multiple list to show in one page, MUST set 'id' in |
146
|
|
|
* config. |
147
|
|
|
* |
148
|
|
|
* @param Smarty $tpl |
149
|
|
|
* @param array $configs |
150
|
|
|
*/ |
151
|
|
|
public function __construct($tpl, array $configs = []) |
152
|
|
|
{ |
153
|
|
|
$this->tpl = $tpl; |
154
|
|
|
|
155
|
|
|
// Config will effect setData, so set it first. |
156
|
|
|
$this->setDefaultConfigs(); |
157
|
|
|
$this->setConfigs($configs); |
158
|
|
|
|
159
|
|
|
$this->tpl->assignByRef("{$this->tplVarPrefix}Config", $this->configs); |
160
|
|
|
$this->tpl->assignByRef("{$this->tplVarPrefix}Info", $this->info); |
161
|
|
|
$this->tpl->assignByRef("{$this->tplVarPrefix}Url", $this->url); |
162
|
|
|
|
163
|
|
|
$this->tpl->assignByRef("{$this->tplVarPrefix}Data", $this->listData); |
164
|
|
|
$this->tpl->assignByRef("{$this->tplVarPrefix}Title", $this->listTitle); |
165
|
|
|
} |
166
|
|
|
|
167
|
|
|
|
168
|
|
|
/** |
169
|
|
|
* Fit each row in data with given keys |
170
|
|
|
* |
171
|
|
|
* If row index is not in given keys, it will be dropped. |
172
|
|
|
* If given keys is not in row index, it will be created with value |
173
|
|
|
* $this->configs['fitEmpty']. |
174
|
|
|
* |
175
|
|
|
* @param array $key |
176
|
|
|
*/ |
177
|
|
|
protected function fitData(array $key) |
178
|
|
|
{ |
179
|
|
|
// Do search on first row for speed |
180
|
|
|
$keyAdd = []; |
181
|
|
|
$keyDel = []; |
182
|
|
|
reset($this->listData); |
183
|
|
|
$row = current($this->listData); |
184
|
|
|
|
185
|
|
|
// Drop key not in key list |
186
|
|
|
foreach ((array)$row as $k => $v) { |
187
|
|
|
if (!in_array($k, $key)) { |
188
|
|
|
$keyDel[] = $k; |
189
|
|
|
} |
190
|
|
|
} |
191
|
|
|
// Add key not exists |
192
|
|
|
foreach ($key as $k) { |
193
|
|
|
// isset() will return false if array key exists but value is null |
194
|
|
|
if (!array_key_exists($k, $row)) { |
195
|
|
|
$keyAdd[] = $k; |
196
|
|
|
} |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
|
200
|
|
|
if (empty($keyAdd) && empty($keyDel)) { |
201
|
|
|
return; |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
|
205
|
|
|
$fitEmpty = $this->configs['fitEmpty']; |
206
|
|
|
foreach ($this->listData as &$row) { |
207
|
|
|
foreach ((array)$keyDel as $k) { |
208
|
|
|
unset($row[$k]); |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
foreach ((array)$keyAdd as $k) { |
212
|
|
|
$row[$k] = $fitEmpty; |
213
|
|
|
} |
214
|
|
|
} |
215
|
|
|
unset($row); |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
|
219
|
|
|
/** |
220
|
|
|
* Fit title with given keys |
221
|
|
|
* |
222
|
|
|
* Drop title value not in given keys, and create new if given keys is not |
223
|
|
|
* exists in title array. |
224
|
|
|
* |
225
|
|
|
* @param array $key |
226
|
|
|
*/ |
227
|
|
|
protected function fitTitle(array $key) |
228
|
|
|
{ |
229
|
|
|
// Title index not in key list |
230
|
|
|
foreach ($this->listTitle as $k => $v) { |
231
|
|
|
if (!in_array($k, $key)) { |
232
|
|
|
unset($this->listTitle[$k]); |
233
|
|
|
} |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
// Key not exist in title |
237
|
|
|
foreach ($key as $k) { |
238
|
|
|
if (!isset($this->listTitle[$k])) { |
239
|
|
|
// Title value is same as key |
240
|
|
|
$this->listTitle[$k] = $k; |
241
|
|
|
} |
242
|
|
|
} |
243
|
|
|
} |
244
|
|
|
|
245
|
|
|
|
246
|
|
|
/** |
247
|
|
|
* Fit data and title if their key are different |
248
|
|
|
* |
249
|
|
|
* Notice: data have multi row(2 dim), and 2nd dimension must use assoc |
250
|
|
|
* index. Title have only 1 row(1 dim), integer or assoc indexed. |
251
|
|
|
* |
252
|
|
|
* @see $config['fitMode'] |
253
|
|
|
*/ |
254
|
|
|
protected function fitTitleWithData() |
255
|
|
|
{ |
256
|
|
|
if (empty($this->listData) || empty($this->listTitle)) { |
257
|
|
|
return; |
258
|
|
|
} |
259
|
|
|
|
260
|
|
|
// Will compare by array keys |
261
|
|
|
// For data, will use it's first/current row, |
262
|
|
|
// For title, will use original value if hasn't assoc index |
263
|
|
|
$keyOfData = array_keys(current($this->listData)); |
264
|
|
|
$keyOfTitle = array_keys($this->listTitle); |
265
|
|
|
|
266
|
|
|
if ($keyOfData == $keyOfTitle) { |
267
|
|
|
return; |
268
|
|
|
} |
269
|
|
|
|
270
|
|
|
|
271
|
|
|
$ar = []; |
272
|
|
View Code Duplication |
switch ($this->configs['fitMode']) { |
|
|
|
|
273
|
|
|
case self::FIT_TO_TITLE: |
274
|
|
|
$ar = $keyOfTitle; |
275
|
|
|
break; |
276
|
|
|
|
277
|
|
|
case self::FIT_TO_DATA: |
278
|
|
|
$ar = $keyOfData; |
279
|
|
|
break; |
280
|
|
|
|
281
|
|
|
case self::FIT_INTERSECTION: |
282
|
|
|
$ar = array_intersect($keyOfTitle, $keyOfData); |
283
|
|
|
break; |
284
|
|
|
|
285
|
|
|
case self::FIT_UNION: |
286
|
|
|
$ar = array_unique(array_merge($keyOfTitle, $keyOfData)); |
287
|
|
|
break; |
288
|
|
|
default: |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
$this->fitTitle($ar); |
292
|
|
|
$this->fitData($ar); |
293
|
|
|
} |
294
|
|
|
|
295
|
|
|
|
296
|
|
|
/** |
297
|
|
|
* Format list data use closure function |
298
|
|
|
* |
299
|
|
|
* Closure function $formatFunction must have 1 param and define as |
300
|
|
|
* reference, eg: function (&row) {}. |
301
|
|
|
* |
302
|
|
|
* @param callback $formatFunction |
303
|
|
|
* @return $this |
304
|
|
|
*/ |
305
|
|
|
public function formatData($formatFunction) |
306
|
|
|
{ |
307
|
|
|
foreach ($this->listData as &$row) { |
308
|
|
|
$formatFunction($row); |
309
|
|
|
} |
310
|
|
|
unset($row); |
311
|
|
|
|
312
|
|
|
return $this; |
313
|
|
|
} |
314
|
|
|
|
315
|
|
|
|
316
|
|
|
/** |
317
|
|
|
* Generate url with some modification |
318
|
|
|
* |
319
|
|
|
* @param array $modify |
320
|
|
|
* @param array $exclude |
321
|
|
|
* @return string |
322
|
|
|
* @see $this->param |
323
|
|
|
*/ |
324
|
|
|
protected function genUrl($modify = null, $exclude = null) |
325
|
|
|
{ |
326
|
|
|
$param = $this->param; |
327
|
|
|
|
328
|
|
|
foreach ((array)$modify as $k => $v) { |
329
|
|
|
$param[addslashes($k)] = addslashes($v); |
330
|
|
|
} |
331
|
|
|
|
332
|
|
|
foreach ((array)$exclude as $k) { |
333
|
|
|
unset($param[$k]); |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
$url = ''; |
337
|
|
|
foreach ($param as $k => $v) { |
338
|
|
|
$url .= "&$k=$v"; |
339
|
|
|
} |
340
|
|
|
if (!empty($url)) { |
341
|
|
|
$url{0} = '?'; |
342
|
|
|
} |
343
|
|
|
$url = $this->url['base'] . $url; |
344
|
|
|
|
345
|
|
|
return $url; |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
|
349
|
|
|
/** |
350
|
|
|
* Get full output html |
351
|
|
|
* |
352
|
|
|
* @return string |
353
|
|
|
*/ |
354
|
|
|
public function getHtml() |
355
|
|
|
{ |
356
|
|
|
$this->readRequest(true); |
357
|
|
|
$this->setPager(); |
358
|
|
|
|
359
|
|
|
return $this->tpl->fetch($this->configs['tpl']); |
360
|
|
|
} |
361
|
|
|
|
362
|
|
|
|
363
|
|
|
/** |
364
|
|
|
* Get config for generate SQL |
365
|
|
|
* |
366
|
|
|
* The result will use as config in SqlGenerator, include: |
367
|
|
|
* - LIMIT |
368
|
|
|
* - ORDERBY |
369
|
|
|
* |
370
|
|
|
* When there are multiple list on single page, second list must set |
371
|
|
|
* $forcenew to true. |
372
|
|
|
* |
373
|
|
|
* @param boolean $forcenew |
374
|
|
|
* @return array |
375
|
|
|
* @see Fwlib\Db\SqlGenerator |
376
|
|
|
*/ |
377
|
|
|
public function getSqlConfig($forcenew = false) |
378
|
|
|
{ |
379
|
|
|
$this->readRequest($forcenew); |
380
|
|
|
|
381
|
|
|
$ar = []; |
382
|
|
|
|
383
|
|
|
$ar['LIMIT'] = $this->configs['pageSize'] * ($this->info['page'] - 1) |
384
|
|
|
. ', ' . $this->configs['pageSize']; |
385
|
|
|
|
386
|
|
|
if (!empty($this->configs['orderBy'])) { |
387
|
|
|
// orderBy_idx is column name |
388
|
|
|
$ar['ORDERBY'] = $this->configs['orderBy'] |
389
|
|
|
. ' ' . $this->configs['orderByDir']; |
390
|
|
|
} |
391
|
|
|
|
392
|
|
|
return $ar; |
393
|
|
|
} |
394
|
|
|
|
395
|
|
|
|
396
|
|
|
/** |
397
|
|
|
* Read http param and parse |
398
|
|
|
* |
399
|
|
|
* @param boolean $forcenew |
400
|
|
|
* @return $this |
401
|
|
|
*/ |
402
|
|
|
protected function readRequest($forcenew = false) |
403
|
|
|
{ |
404
|
|
|
$arrayUtil = $this->getUtilContainer()->getArray(); |
405
|
|
|
|
406
|
|
|
// Avoid duplicate |
407
|
|
|
if (!empty($this->url['base']) && !$forcenew) { |
408
|
|
|
return $this; |
409
|
|
|
} |
410
|
|
|
|
411
|
|
|
|
412
|
|
|
$this->param = &$_GET; |
413
|
|
|
if (!get_magic_quotes_gpc()) { |
414
|
|
|
foreach ($this->param as &$value) { |
415
|
|
|
$value = addslashes($value); |
416
|
|
|
} |
417
|
|
|
unset($value); |
418
|
|
|
} |
419
|
|
|
|
420
|
|
|
// :NOTICE: Will got only url of backend if ProxyPass used |
421
|
|
|
// Can treat as below before new ListTable obj. |
422
|
|
|
/* |
423
|
|
|
$ar = array('/needless1/', '/needless1/'); |
424
|
|
|
$_SERVER['REQUEST_URI'] = str_replace($ar, '/', $_SERVER['REQUEST_URI']); |
425
|
|
|
$_SERVER['SCRIPT_NAME'] = str_replace($ar, '/', $_SERVER['SCRIPT_NAME']); |
426
|
|
|
*/ |
427
|
|
|
|
428
|
|
|
$httpUtil = $this->getUtilContainer()->getHttp(); |
429
|
|
|
$this->url['base'] = $httpUtil->getSelfUrl(false); |
430
|
|
|
|
431
|
|
|
$page = $arrayUtil->getIdx($this->param, $this->configs['pageParam'], 1); |
432
|
|
|
$this->setPage($page); |
433
|
|
|
|
434
|
|
|
|
435
|
|
|
// Always treat orderBy |
436
|
|
|
$orderBy = ''; |
437
|
|
|
$dir = ''; |
438
|
|
|
if (isset($this->param[$this->configs['orderByParam']])) { |
439
|
|
|
$orderBy = $this->param[$this->configs['orderByParam']]; |
440
|
|
|
$dir = $arrayUtil->getIdx( |
441
|
|
|
$this->param, |
442
|
|
|
$this->configs['orderByParam'] . 'Dir', |
443
|
|
|
'' |
444
|
|
|
); |
445
|
|
|
} |
446
|
|
|
$this->setOrderBy($orderBy, $dir); |
447
|
|
|
|
448
|
|
|
return $this; |
449
|
|
|
} |
450
|
|
|
|
451
|
|
|
|
452
|
|
|
/** |
453
|
|
|
* Set single config |
454
|
|
|
* |
455
|
|
|
* @param string $key |
456
|
|
|
* @param mixed $value |
457
|
|
|
* @return ListTable |
458
|
|
|
*/ |
459
|
|
|
public function setConfig($key, $value) |
460
|
|
|
{ |
461
|
|
|
$this->configs[$key] = $value; |
462
|
|
|
|
463
|
|
|
if ('class' == $key) { |
464
|
|
|
$this->setId($this->configs['id'], $value); |
465
|
|
|
|
466
|
|
|
} elseif ('id' == $key) { |
467
|
|
|
$this->setId($value, $this->configs['class']); |
468
|
|
|
} |
469
|
|
|
|
470
|
|
|
return $this; |
471
|
|
|
} |
472
|
|
|
|
473
|
|
|
|
474
|
|
|
/** |
475
|
|
|
* Set multiple configs |
476
|
|
|
* |
477
|
|
|
* @param array $configs |
478
|
|
|
* @return ListTable |
479
|
|
|
*/ |
480
|
|
|
public function setConfigs(array $configs) |
481
|
|
|
{ |
482
|
|
|
$this->configs = array_merge($this->configs, $configs); |
483
|
|
|
|
484
|
|
|
if (isset($configs['class']) || isset($configs['id'])) { |
485
|
|
|
$this->setId( |
486
|
|
|
isset($configs['id']) ? $configs['id'] |
487
|
|
|
: $this->configs['id'], |
488
|
|
|
isset($configs['class']) ? $configs['class'] |
489
|
|
|
: $this->configs['class'] |
490
|
|
|
); |
491
|
|
|
} |
492
|
|
|
|
493
|
|
|
|
494
|
|
|
return $this; |
495
|
|
|
} |
496
|
|
|
|
497
|
|
|
|
498
|
|
|
/** |
499
|
|
|
* Set list data and title |
500
|
|
|
* |
501
|
|
|
* @param array $listData |
502
|
|
|
* @param array $listTitle |
503
|
|
|
* @param boolean $updateTotalRows |
504
|
|
|
* @return $this |
505
|
|
|
*/ |
506
|
|
|
public function setData($listData, $listTitle = null, $updateTotalRows = false) |
507
|
|
|
{ |
508
|
|
|
$this->listData = $listData; |
509
|
|
|
if ($updateTotalRows || (-1 == $this->info['totalRows'])) { |
510
|
|
|
$this->info['totalRows'] = count($listData); |
511
|
|
|
} |
512
|
|
|
|
513
|
|
|
if (!is_null($listTitle)) { |
514
|
|
|
$this->listTitle = $listTitle; |
515
|
|
|
} |
516
|
|
|
|
517
|
|
|
// Same number of items maybe index diff, so always do fit. |
518
|
|
|
$this->fitTitleWithData(); |
519
|
|
|
|
520
|
|
|
return $this; |
521
|
|
|
} |
522
|
|
|
|
523
|
|
|
|
524
|
|
|
/** |
525
|
|
|
* Set db query |
526
|
|
|
* |
527
|
|
|
* Will query total rows and list data by set db connection and config, |
528
|
|
|
* will overwrite exists $listData. |
529
|
|
|
* |
530
|
|
|
* @param Adodb $db |
531
|
|
|
* @param array $config |
532
|
|
|
* @return $this |
533
|
|
|
*/ |
534
|
|
|
public function setDbQuery($db, $config) |
535
|
|
|
{ |
536
|
|
|
// Get totalRows |
537
|
|
|
$this->info['totalRows'] = $db->execute( |
538
|
|
|
array_merge($config, ['SELECT' => 'COUNT(1) AS c']) |
539
|
|
|
) |
540
|
|
|
->fields['c']; |
541
|
|
|
|
542
|
|
|
// Query data |
543
|
|
|
$rs = $db->execute( |
544
|
|
|
array_merge($config, $this->getSqlConfig(true)) |
545
|
|
|
); |
546
|
|
|
$this->listData = $rs->GetArray(); |
547
|
|
|
|
548
|
|
|
$this->fitTitleWithData(); |
549
|
|
|
|
550
|
|
|
return $this; |
551
|
|
|
} |
552
|
|
|
|
553
|
|
|
|
554
|
|
|
/** |
555
|
|
|
* Set default configs |
556
|
|
|
*/ |
557
|
|
|
protected function setDefaultConfigs() |
558
|
|
|
{ |
559
|
|
|
/** @noinspection SpellCheckingInspection */ |
560
|
|
|
$this->setConfigs( |
561
|
|
|
[ |
562
|
|
|
// Notice: this is NOT actual class and id used in template, |
563
|
|
|
// see $this->info and $this->setId() for details. |
564
|
|
|
|
565
|
|
|
// Classname for root element, and prefix of inherit elements |
566
|
|
|
// Should not be empty. |
567
|
|
|
'class' => 'list-table', |
568
|
|
|
// Id of this list, default 1, will use class as prefix if not empty. |
569
|
|
|
// Can be string or integer, should not be empty. |
570
|
|
|
'id' => 1, |
571
|
|
|
//'code_prefix' => 'fl_lt', // Used in id/class in html and css. |
572
|
|
|
|
573
|
|
|
|
574
|
|
|
// Color schema: light blue |
575
|
|
|
// Color should assign using CSS as possible as it could, |
576
|
|
|
// below color will use jQuery to assign. |
577
|
|
|
// :TODO: Use pure CSS ? |
578
|
|
|
'enableColorTh' => true, |
579
|
|
|
'colorBgTh' => '#d0dcff', // 表头(thead) |
580
|
|
|
|
581
|
|
|
'enableColorTr' => true, |
582
|
|
|
'colorBgTrEven' => '#fff', // 偶数行 |
583
|
|
|
'colorBgTrOdd' => '#eef2ff', // 奇数行,tbody后从0开始算 |
584
|
|
|
|
585
|
|
|
'enableColorHover' => true, |
586
|
|
|
'colorBgTrHover' => '#e3e3de', // 鼠标指向时变色 |
587
|
|
|
//'color_bg_th' => '#d0dcff', // 表头(thead) |
588
|
|
|
//'color_bg_tr_even' => '#fff', // 偶数行 |
589
|
|
|
//'color_bg_tr_hover' => '#e3e3de', // 鼠标指向时变色 |
590
|
|
|
//'color_bg_tr_odd' => '#eef2ff', // 奇数行,tbody后从0开始算 |
591
|
|
|
|
592
|
|
|
|
593
|
|
|
// Data and title fit mode, default FIT_TO_TITLE |
594
|
|
|
'fitMode' => self::FIT_TO_TITLE, |
595
|
|
|
|
596
|
|
|
// If a value in data is empty, display with this value. |
597
|
|
|
// Not for title, which will use field name. |
598
|
|
|
'fitEmpty' => ' ', |
599
|
|
|
//'fit_data_title' => 0, |
600
|
|
|
//'fit_empty' => ' ', |
601
|
|
|
|
602
|
|
|
|
603
|
|
|
// Enable orderBy on those column, empty to disable orderBy |
604
|
|
|
// Format: {[column, direction],} |
605
|
|
|
// First [] is default, and default direction is ASC. |
606
|
|
|
'orderByColumn' => [], |
607
|
|
|
// Which column to orderBy, |
608
|
|
|
'orderBy' => '', |
609
|
|
|
// Orderby direction, ASC or DESC |
610
|
|
|
'orderByDir' => 'ASC', |
611
|
|
|
// Get param for orderBy |
612
|
|
|
'orderByParam' => 'ob', |
613
|
|
|
// Preserved, will be auto generated if orderBy enabled |
614
|
|
|
'orderByText' => '', |
615
|
|
|
// Orderby text/symbol for ASC and DESC |
616
|
|
|
'orderByTextAsc' => '↑', |
617
|
|
|
'orderByTextDesc' => '↓', |
618
|
|
|
// More orderBy symbol |
619
|
|
|
// ← = ← ↑ = ↑ → = → ↓ = ↓ |
620
|
|
|
// ∆ = ∆ ∇ = ∇ |
621
|
|
|
//'orderby' => 0, // 0=off, 1=on |
622
|
|
|
//'orderby_dir' => 'ASC', |
623
|
|
|
//'orderby_idx' => '', // Idx of th ar |
624
|
|
|
//'orderby_param' => 'o', |
625
|
|
|
//'orderby_text' => '', |
626
|
|
|
//'orderby_text_asc' => '↑', |
627
|
|
|
//'orderby_text_desc' => '↓', |
628
|
|
|
|
629
|
|
|
|
630
|
|
|
// Get param for identify current page |
631
|
|
|
'pageParam' => 'p', |
632
|
|
|
// How many rows to display each page |
633
|
|
|
'pageSize' => 10, |
634
|
|
|
//'page_cur' => 1, |
635
|
|
|
//'pageParam' => 'p',// Used in url to set page no. |
636
|
|
|
//'page_size' => 10, |
637
|
|
|
|
638
|
|
|
|
639
|
|
|
// Show pager above list table |
640
|
|
|
'pagerAbove' => true, |
641
|
|
|
// Show pager below list table |
642
|
|
|
'pagerBelow' => true, |
643
|
|
|
// Pager message template |
644
|
|
|
'pagerTextFirst' => '首页', |
645
|
|
|
'pagerTextPrev' => '上一页', |
646
|
|
|
'pagerTextNext' => '下一页', |
647
|
|
|
'pagerTextLast' => '尾页', |
648
|
|
|
'pagerTextBody' => |
649
|
|
|
'共{totalRows}条记录,每页显示{pageSize}条,当前为第{page}/{pageMax}页', |
650
|
|
|
'pagerTextJump1' => '转到第', |
651
|
|
|
'pagerTextJump2' => '页', |
652
|
|
|
'pagerTextJumpButton' => '转', |
653
|
|
|
// Spacer between pager text plistTitles |
654
|
|
|
'pagerTextSpacer' => ' | ', |
655
|
|
|
//'pager' => false, // Is or not use pager |
656
|
|
|
//'pager_bottom' => true, // Is or not use pager bottom, used when pager=true |
657
|
|
|
// This is a message template |
658
|
|
|
// When display, use key append by '_value' |
659
|
|
|
//'pager_text_cur' => |
660
|
|
|
// '共{rows_total}条记录,每页显示{page_size}条,当前为第{page_cur}/{page_max}页', |
661
|
|
|
//'pager_text_first' => '首页', |
662
|
|
|
//'pager_text_goto1' => '转到第', |
663
|
|
|
//'pager_text_goto2' => '页', |
664
|
|
|
//'pager_text_goto3' => '转', |
665
|
|
|
//'pager_text_last' => '尾页', |
666
|
|
|
//'pager_text_next' => '下一页', |
667
|
|
|
//'pager_text_prev' => '上一页', |
668
|
|
|
//'pager_text_spacer' => ' | ', // To be between below texts. |
669
|
|
|
//'pager_top' => true, // Is or not use pager top, used when pager=true |
670
|
|
|
|
671
|
|
|
|
672
|
|
|
//'rows_total' => 0, |
673
|
|
|
// Template file path |
674
|
|
|
'tpl' => __DIR__ . '/list-table.tpl', |
675
|
|
|
|
676
|
|
|
// Add custom string in td/th/tr tag, eg: nowrap='nowrap' |
677
|
|
|
// td/th can use index same with data array index, |
678
|
|
|
// tr can use int index whose value is string too. |
679
|
|
|
// :TODO: tr int index is converted to string ? |
680
|
|
|
// For tr of th row, use th instead. |
681
|
|
|
'tdAdd' => [], |
682
|
|
|
'thAdd' => [], |
683
|
|
|
'trAdd' => [], |
684
|
|
|
//'td_add' => array(), |
685
|
|
|
//'th_add' => array(), |
686
|
|
|
//'tr_add' => array(), |
687
|
|
|
] |
688
|
|
|
); |
689
|
|
|
} |
690
|
|
|
|
691
|
|
|
|
692
|
|
|
/** |
693
|
|
|
* Set id and class of list table |
694
|
|
|
* |
695
|
|
|
* Id and class should not have space or other special chars not allowed |
696
|
|
|
* by js and css in it. |
697
|
|
|
* |
698
|
|
|
* @param string $id |
699
|
|
|
* @param string $class |
700
|
|
|
* @return $this |
701
|
|
|
*/ |
702
|
|
|
public function setId($id, $class = null) |
703
|
|
|
{ |
704
|
|
|
$class = trim($class); |
705
|
|
|
// Class should not be empty |
706
|
|
|
if (empty($class)) { |
707
|
|
|
$class = $this->configs['class']; // For later use |
708
|
|
|
} else { |
709
|
|
|
$this->configs['class'] = $class; |
710
|
|
|
} |
711
|
|
|
|
712
|
|
|
$this->info['class'] = $class; |
713
|
|
|
|
714
|
|
|
// Class may have multiple value split by space, use first one as |
715
|
|
|
// prefix of other elements. |
716
|
|
|
if (false !== strpos($class, ' ')) { |
717
|
|
|
$class = strstr($class, ' ', true); |
718
|
|
|
} |
719
|
|
|
$this->info['classPrefix'] = $class . '__'; |
720
|
|
|
|
721
|
|
|
|
722
|
|
|
$id = trim($id); |
723
|
|
|
// Avoid 0, which is empty |
724
|
|
|
if (0 == strlen($id)) { |
725
|
|
|
$id = 1; |
726
|
|
|
} |
727
|
|
|
$this->configs['id'] = $id; |
728
|
|
|
$this->info['idPrefix'] = $this->info['classPrefix'] . $id . '__'; |
729
|
|
|
$this->info['id'] = $this->info['classPrefix'] . $id; |
730
|
|
|
|
731
|
|
|
|
732
|
|
|
// Change pageParam, eg: p1, pa |
733
|
|
|
if ('0' != $id && '1' != $id) { |
734
|
|
|
$this->configs['pageParam'] = 'p' . $id; |
735
|
|
|
} |
736
|
|
|
// Useless, readRequest() will call it |
737
|
|
|
//$this->setPage(); |
738
|
|
|
|
739
|
|
|
// Change orderBy param |
740
|
|
|
$this->configs['orderByParam'] = 'ob' . $id; |
741
|
|
|
|
742
|
|
|
return $this; |
743
|
|
|
} |
744
|
|
|
|
745
|
|
|
|
746
|
|
|
/** |
747
|
|
|
* Set orderBy info |
748
|
|
|
* |
749
|
|
|
* Did not validate orderBy key exists in data or title array. |
750
|
|
|
* |
751
|
|
|
* @param mixed $key |
752
|
|
|
* @param string $dir ASC/DESC |
753
|
|
|
*/ |
754
|
|
|
public function setOrderBy($key = null, $dir = null) |
755
|
|
|
{ |
756
|
|
|
// Parse orderBy config |
757
|
|
|
$orderByColumn = []; |
758
|
|
|
foreach ((array)$this->configs['orderByColumn'] as $v) { |
759
|
|
|
$orderByColumn[$v[0]] = [ |
760
|
|
|
$v[0], |
761
|
|
|
(isset($v[1])) ? $v[1] : 'ASC', |
762
|
|
|
]; |
763
|
|
|
} |
764
|
|
|
$this->info['orderByColumn'] = $orderByColumn; |
765
|
|
|
|
766
|
|
|
|
767
|
|
|
// Check orderBy param, if fail, use config default |
768
|
|
|
if (!isset($orderByColumn[$key])) { |
769
|
|
|
list($key, $dir) = current($orderByColumn); |
770
|
|
|
} elseif (empty($dir)) { |
771
|
|
|
$dir = $orderByColumn[$key][1]; |
772
|
|
|
} |
773
|
|
|
$this->configs['orderBy'] = $key; |
774
|
|
|
|
775
|
|
|
|
776
|
|
|
$dir = strtoupper($dir); |
777
|
|
|
if ('ASC' == $dir) { |
778
|
|
|
$dirReverse = 'DESC'; |
779
|
|
|
$this->configs['orderByDir'] = 'ASC'; |
780
|
|
|
$this->configs['orderByText'] = $this->configs['orderByTextAsc']; |
781
|
|
|
} else { |
782
|
|
|
$dirReverse = 'ASC'; |
783
|
|
|
$this->configs['orderByDir'] = 'DESC'; |
784
|
|
|
$this->configs['orderByText'] = $this->configs['orderByTextDesc']; |
785
|
|
|
} |
786
|
|
|
|
787
|
|
|
|
788
|
|
|
// Url param |
789
|
|
|
$ob = $this->configs['orderByParam']; |
790
|
|
|
// Change orderBy will clear page param |
791
|
|
|
// Orderby index is appended in template by each th, remove here |
792
|
|
|
$this->url['obCur'] = $this->genUrl( |
793
|
|
|
["{$ob}Dir" => $dirReverse], |
794
|
|
|
[$ob, $this->configs['pageParam']] |
795
|
|
|
); |
796
|
|
|
|
797
|
|
|
// Other column orderBy will clear direction |
798
|
|
|
// Added pageParam is dummy, to keep url start with '?', fit tpl later |
799
|
|
|
$this->url['obOther'] = $this->genUrl( |
800
|
|
|
[$this->configs['pageParam'] => 1], |
801
|
|
|
[$ob, "{$ob}Dir"] |
802
|
|
|
); |
803
|
|
|
} |
804
|
|
|
|
805
|
|
|
|
806
|
|
|
/** |
807
|
|
|
* Check and set current page |
808
|
|
|
* |
809
|
|
|
* @param int $page Page num param come from outer |
810
|
|
|
* @return $this |
811
|
|
|
*/ |
812
|
|
|
protected function setPage($page = null) |
813
|
|
|
{ |
814
|
|
|
$arrayUtil = $this->getUtilContainer()->getArray(); |
815
|
|
|
|
816
|
|
|
if (is_null($page)) { |
817
|
|
|
$page = $arrayUtil->getIdx( |
818
|
|
|
$this->param, |
819
|
|
|
$this->configs['pageParam'], |
820
|
|
|
1 |
821
|
|
|
); |
822
|
|
|
} |
823
|
|
|
$page = intval($page); |
824
|
|
|
|
825
|
|
|
|
826
|
|
|
// Compare with min and max page number |
827
|
|
|
if (1 > $page) { |
828
|
|
|
$page = 1; |
829
|
|
|
} |
830
|
|
|
if (0 < $this->info['totalRows'] |
831
|
|
|
&& 0 < $this->configs['pageSize'] |
832
|
|
|
) { |
833
|
|
|
$this->info['pageMax'] = |
834
|
|
|
ceil($this->info['totalRows'] / $this->configs['pageSize']); |
835
|
|
|
$page = min($page, $this->info['pageMax']); |
836
|
|
|
} |
837
|
|
|
|
838
|
|
|
|
839
|
|
|
$this->info['page'] = $page; |
840
|
|
|
return $this; |
841
|
|
|
} |
842
|
|
|
|
843
|
|
|
|
844
|
|
|
/** |
845
|
|
|
* Set pager info |
846
|
|
|
* |
847
|
|
|
* Will execute even pager above/below are both disabled. |
848
|
|
|
* |
849
|
|
|
* @return $this |
850
|
|
|
* @see $config |
851
|
|
|
*/ |
852
|
|
|
protected function setPager() |
853
|
|
|
{ |
854
|
|
|
$page = $this->info['page']; |
855
|
|
|
$pageMax = $this->info['pageMax']; |
856
|
|
|
$pageSize = $this->configs['pageSize']; |
857
|
|
|
$totalRows = $this->info['totalRows']; |
858
|
|
|
|
859
|
|
|
|
860
|
|
|
// If data rows exceeds pageSize, trim it |
861
|
|
|
if (count($this->listData) > $pageSize) { |
862
|
|
|
// If page = 3/5, trim page 1, 2 first |
863
|
|
|
for ($i = 0; $i < ($page - 1) * $pageSize; $i ++) { |
864
|
|
|
unset($this->listData[$i]); |
865
|
|
|
} |
866
|
|
|
// Then trim page 4, 5 |
867
|
|
|
for ($i = $page * $pageSize; $i < $pageMax * $pageSize; $i ++) { |
868
|
|
|
unset($this->listData[$i]); |
869
|
|
|
} |
870
|
|
|
} |
871
|
|
|
|
872
|
|
|
$this->info['pagerTextBody'] = str_replace( |
873
|
|
|
['{page}', '{pageMax}', '{totalRows}', '{pageSize}'], |
874
|
|
|
[$page, $pageMax, $totalRows, $pageSize], |
875
|
|
|
$this->configs['pagerTextBody'] |
876
|
|
|
); |
877
|
|
|
|
878
|
|
|
// Generate url for pager |
879
|
|
|
//$this->url['base'] = GetSelfUrl(true); // Move to readRequest() |
880
|
|
|
if (1 < $page) { |
881
|
|
|
// Not first page |
882
|
|
|
$this->url['pageFirst'] = $this->genUrl( |
883
|
|
|
[$this->configs['pageParam'] => 1] |
884
|
|
|
); |
885
|
|
|
$this->url['pagePrev'] = $this->genUrl( |
886
|
|
|
[$this->configs['pageParam'] => $page - 1] |
887
|
|
|
); |
888
|
|
|
} else { |
889
|
|
|
$this->url['pageFirst'] = ''; |
890
|
|
|
$this->url['pagePrev'] = ''; |
891
|
|
|
} |
892
|
|
|
if ($page < $pageMax) { |
893
|
|
|
// Not last page |
894
|
|
|
$this->url['pageNext'] = $this->genUrl( |
895
|
|
|
[$this->configs['pageParam'] => $page + 1] |
896
|
|
|
); |
897
|
|
|
$this->url['pageLast'] = $this->genUrl( |
898
|
|
|
[$this->configs['pageParam'] => $pageMax] |
899
|
|
|
); |
900
|
|
|
} else { |
901
|
|
|
$this->url['pageNext'] = ''; |
902
|
|
|
$this->url['pageLast'] = ''; |
903
|
|
|
} |
904
|
|
|
|
905
|
|
|
// Form submit target url |
906
|
|
|
$this->url['form'] = $this->genUrl( |
907
|
|
|
null, |
908
|
|
|
[$this->configs['pageParam']] |
909
|
|
|
); |
910
|
|
|
|
911
|
|
|
// Assign hidden input |
912
|
|
|
if (!empty($this->param)) { |
913
|
|
|
$s = ''; |
914
|
|
|
foreach ($this->param as $k => $v) { |
915
|
|
|
$s .= "<input type='hidden' name='$k' value='$v' />\n"; |
916
|
|
|
} |
917
|
|
|
$this->tpl->assign("{$this->tplVarPrefix}PagerHidden", $s); |
918
|
|
|
} |
919
|
|
|
|
920
|
|
|
return $this; |
921
|
|
|
} |
922
|
|
|
|
923
|
|
|
|
924
|
|
|
/** |
925
|
|
|
* Set ListTable title |
926
|
|
|
* |
927
|
|
|
* Usually used together with setDbQuery(), which need not set listData |
928
|
|
|
* from outside(use setData() method). |
929
|
|
|
* |
930
|
|
|
* In common this method should call before setDbQuery(). |
931
|
|
|
* |
932
|
|
|
* @param array $title |
933
|
|
|
* @return $this |
934
|
|
|
*/ |
935
|
|
|
public function setTitle($title) |
936
|
|
|
{ |
937
|
|
|
$this->listTitle = $title; |
938
|
|
|
|
939
|
|
|
return $this; |
940
|
|
|
} |
941
|
|
|
|
942
|
|
|
|
943
|
|
|
/** |
944
|
|
|
* Set totalRows |
945
|
|
|
* |
946
|
|
|
* @param int $totalRows |
947
|
|
|
* @return $this |
948
|
|
|
*/ |
949
|
|
|
public function setTotalRows($totalRows) |
950
|
|
|
{ |
951
|
|
|
$this->info['totalRows'] = $totalRows; |
952
|
|
|
|
953
|
|
|
return $this; |
954
|
|
|
} |
955
|
|
|
} |
956
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.