@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class AphrontTableView extends AphrontView {
4
5 protected $data;
6 protected $headers;
7 protected $shortHeaders = array();
8 protected $rowClasses = array();
9 protected $columnClasses = array();
10 protected $cellClasses = array();
11 protected $zebraStripes = true;
12 protected $noDataString;
13 protected $className;
14 protected $notice;
15 protected $columnVisibility = array();
16 private $deviceVisibility = array();
17
18 private $columnWidths = array();
19
20 protected $sortURI;
21 protected $sortParam;
22 protected $sortSelected;
23 protected $sortReverse;
24 protected $sortValues = array();
25 private $deviceReadyTable;
26
27 private $rowDividers = array();
28
29 public function __construct(array $data) {
30 $this->data = $data;
31 }
32
33 public function setHeaders(array $headers) {
34 $this->headers = $headers;
35 return $this;
36 }
37
38 public function setColumnClasses(array $column_classes) {
39 $this->columnClasses = $column_classes;
40 return $this;
41 }
42
43 public function setRowClasses(array $row_classes) {
44 $this->rowClasses = $row_classes;
45 return $this;
46 }
47
48 public function setCellClasses(array $cell_classes) {
49 $this->cellClasses = $cell_classes;
50 return $this;
51 }
52
53 public function setColumnWidths(array $widths) {
54 $this->columnWidths = $widths;
55 return $this;
56 }
57
58 public function setRowDividers(array $dividers) {
59 $this->rowDividers = $dividers;
60 return $this;
61 }
62
63 public function setNoDataString($no_data_string) {
64 $this->noDataString = $no_data_string;
65 return $this;
66 }
67
68 public function setClassName($class_name) {
69 $this->className = $class_name;
70 return $this;
71 }
72
73 public function setNotice($notice) {
74 $this->notice = $notice;
75 return $this;
76 }
77
78 public function setZebraStripes($zebra_stripes) {
79 $this->zebraStripes = $zebra_stripes;
80 return $this;
81 }
82
83 public function setColumnVisibility(array $visibility) {
84 $this->columnVisibility = $visibility;
85 return $this;
86 }
87
88 public function setDeviceVisibility(array $device_visibility) {
89 $this->deviceVisibility = $device_visibility;
90 return $this;
91 }
92
93 public function setDeviceReadyTable($ready) {
94 $this->deviceReadyTable = $ready;
95 return $this;
96 }
97
98 public function setShortHeaders(array $short_headers) {
99 $this->shortHeaders = $short_headers;
100 return $this;
101 }
102
103 /**
104 * Parse a sorting parameter:
105 *
106 * list($sort, $reverse) = AphrontTableView::parseSortParam($sort_param);
107 *
108 * @param string $sort Sort request parameter.
109 * @return array Sort value, sort direction.
110 */
111 public static function parseSort($sort) {
112 return array(ltrim($sort, '-'), preg_match('/^-/', $sort));
113 }
114
115 public function makeSortable(
116 PhutilURI $base_uri,
117 $param,
118 $selected,
119 $reverse,
120 array $sort_values) {
121
122 $this->sortURI = $base_uri;
123 $this->sortParam = $param;
124 $this->sortSelected = $selected;
125 $this->sortReverse = $reverse;
126 $this->sortValues = array_values($sort_values);
127
128 return $this;
129 }
130
131 public function render() {
132 require_celerity_resource('aphront-table-view-css');
133
134 $table = array();
135
136 $col_classes = array();
137 foreach ($this->columnClasses as $key => $class) {
138 if (phutil_nonempty_string($class)) {
139 $col_classes[] = $class;
140 } else {
141 $col_classes[] = null;
142 }
143 }
144
145 $visibility = array_values($this->columnVisibility);
146 $device_visibility = array_values($this->deviceVisibility);
147
148 $column_widths = $this->columnWidths;
149
150 $headers = $this->headers;
151 $short_headers = $this->shortHeaders;
152 $sort_values = $this->sortValues;
153 if ($headers) {
154 while (count($headers) > count($visibility)) {
155 $visibility[] = true;
156 }
157 while (count($headers) > count($device_visibility)) {
158 $device_visibility[] = true;
159 }
160 while (count($headers) > count($short_headers)) {
161 $short_headers[] = null;
162 }
163 while (count($headers) > count($sort_values)) {
164 $sort_values[] = null;
165 }
166
167 $tr = array();
168 foreach ($headers as $col_num => $header) {
169 if (!$visibility[$col_num]) {
170 continue;
171 }
172
173 $classes = array();
174
175 if (!empty($col_classes[$col_num])) {
176 $classes[] = $col_classes[$col_num];
177 }
178
179 if (empty($device_visibility[$col_num])) {
180 $classes[] = 'aphront-table-view-nodevice';
181 }
182
183 if ($sort_values[$col_num] !== null) {
184 $classes[] = 'aphront-table-view-sortable';
185
186 $sort_value = $sort_values[$col_num];
187 $sort_glyph_class = 'aphront-table-down-sort';
188 if ($sort_value == $this->sortSelected) {
189 if ($this->sortReverse) {
190 $sort_glyph_class = 'aphront-table-up-sort';
191 } else {
192 $sort_value = '-'.$sort_value;
193 }
194 $classes[] = 'aphront-table-view-sortable-selected';
195 }
196
197 $sort_glyph = phutil_tag(
198 'span',
199 array(
200 'class' => $sort_glyph_class,
201 ),
202 '');
203
204 $header = phutil_tag(
205 'a',
206 array(
207 'href' => $this->sortURI->alter($this->sortParam, $sort_value),
208 'class' => 'aphront-table-view-sort-link',
209 ),
210 array(
211 $header,
212 ' ',
213 $sort_glyph,
214 ));
215 }
216
217 if ($classes) {
218 $class = implode(' ', $classes);
219 } else {
220 $class = null;
221 }
222
223 if ($short_headers[$col_num] !== null) {
224 $header_nodevice = phutil_tag(
225 'span',
226 array(
227 'class' => 'aphront-table-view-nodevice',
228 ),
229 $header);
230 $header_device = phutil_tag(
231 'span',
232 array(
233 'class' => 'aphront-table-view-device',
234 ),
235 $short_headers[$col_num]);
236
237 $header = hsprintf('%s %s', $header_nodevice, $header_device);
238 }
239
240 $style = null;
241 if (isset($column_widths[$col_num])) {
242 $style = 'width: '.$column_widths[$col_num].';';
243 }
244
245 $tr[] = phutil_tag(
246 'th',
247 array(
248 'class' => $class,
249 'style' => $style,
250 ),
251 $header);
252 }
253 $table[] = phutil_tag('tr', array(), $tr);
254 }
255
256 foreach ($col_classes as $key => $value) {
257
258 if (isset($sort_values[$key]) &&
259 ($sort_values[$key] == $this->sortSelected)) {
260 $value = trim($value.' sorted-column');
261 }
262
263 if ($value !== null) {
264 $col_classes[$key] = $value;
265 }
266 }
267
268 $dividers = $this->rowDividers;
269
270 $data = $this->data;
271 if ($data) {
272 $row_num = 0;
273 $row_idx = 0;
274 foreach ($data as $row) {
275 $is_divider = !empty($dividers[$row_num]);
276
277 $row_size = count($row);
278 while (count($row) > count($col_classes)) {
279 $col_classes[] = null;
280 }
281 while (count($row) > count($visibility)) {
282 $visibility[] = true;
283 }
284 while (count($row) > count($device_visibility)) {
285 $device_visibility[] = true;
286 }
287 $tr = array();
288 // NOTE: Use of a separate column counter is to allow this to work
289 // correctly if the row data has string or non-sequential keys.
290 $col_num = 0;
291 foreach ($row as $value) {
292 if (!$visibility[$col_num]) {
293 ++$col_num;
294 continue;
295 }
296 $class = $col_classes[$col_num];
297 if (empty($device_visibility[$col_num])) {
298 $class = trim($class.' aphront-table-view-nodevice');
299 }
300 if (!empty($this->cellClasses[$row_num][$col_num])) {
301 $class = trim($class.' '.$this->cellClasses[$row_num][$col_num]);
302 }
303
304 if ($is_divider) {
305 $tr[] = phutil_tag(
306 'td',
307 array(
308 'class' => 'row-divider',
309 'colspan' => count($visibility),
310 ),
311 $value);
312 $row_idx = -1;
313 break;
314 }
315
316 $tr[] = phutil_tag(
317 'td',
318 array(
319 'class' => $class,
320 ),
321 $value);
322 ++$col_num;
323 }
324
325 $class = idx($this->rowClasses, $row_num);
326 if ($this->zebraStripes && ($row_idx % 2)) {
327 if ($class !== null) {
328 $class = 'alt alt-'.$class;
329 } else {
330 $class = 'alt';
331 }
332 }
333
334 $table[] = phutil_tag('tr', array('class' => $class), $tr);
335 ++$row_num;
336 ++$row_idx;
337 }
338 } else {
339 $colspan = max(count(array_filter($visibility)), 1);
340 $table[] = phutil_tag(
341 'tr',
342 array('class' => 'no-data'),
343 phutil_tag(
344 'td',
345 array('colspan' => $colspan),
346 coalesce($this->noDataString, pht('No data available.'))));
347 }
348
349 $classes = array();
350 $classes[] = 'aphront-table-view';
351 if ($this->className !== null) {
352 $classes[] = $this->className;
353 }
354
355 if ($this->deviceReadyTable) {
356 $classes[] = 'aphront-table-view-device-ready';
357 }
358
359 if ($this->columnWidths) {
360 $classes[] = 'aphront-table-view-fixed';
361 }
362
363 $notice = null;
364 if ($this->notice) {
365 $notice = phutil_tag(
366 'div',
367 array(
368 'class' => 'aphront-table-notice',
369 ),
370 $this->notice);
371 }
372
373 $html = phutil_tag(
374 'table',
375 array(
376 'class' => implode(' ', $classes),
377 ),
378 $table);
379
380 return phutil_tag_div(
381 'aphront-table-wrap',
382 array(
383 $notice,
384 $html,
385 ));
386 }
387
388 public static function renderSingleDisplayLine($line) {
389
390 // TODO: Is there a cleaner way to do this? We use a relative div with
391 // overflow hidden to provide the bounds, and an absolute span with
392 // white-space: pre to prevent wrapping. We need to append a character
393 // ( -- nonbreaking space) afterward to give the bounds div height
394 // (alternatively, we could hard-code the line height). This is gross but
395 // it's not clear that there's a better approach.
396
397 return phutil_tag(
398 'div',
399 array(
400 'class' => 'single-display-line-bounds',
401 ),
402 array(
403 phutil_tag(
404 'span',
405 array(
406 'class' => 'single-display-line-content',
407 ),
408 $line),
409 "\xC2\xA0",
410 ));
411 }
412
413
414}