Commit | Line | Data |
---|---|---|
7f254ad8 AE |
1 | <?php |
2 | /** | |
3 | * @package dompdf | |
4 | * @link http://dompdf.github.com/ | |
5 | * @author Benj Carson <benjcarson@digitaljunkies.ca> | |
6 | * @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License | |
7 | */ | |
8 | namespace Dompdf\FrameDecorator; | |
9 | ||
10 | use Dompdf\Cellmap; | |
11 | use DOMNode; | |
12 | use Dompdf\Dompdf; | |
13 | use Dompdf\Frame; | |
14 | use Dompdf\Frame\Factory; | |
15 | ||
16 | /** | |
17 | * Decorates Frames for table layout | |
18 | * | |
19 | * @package dompdf | |
20 | */ | |
21 | class Table extends AbstractFrameDecorator | |
22 | { | |
23 | public static $VALID_CHILDREN = array( | |
24 | "table-row-group", | |
25 | "table-row", | |
26 | "table-header-group", | |
27 | "table-footer-group", | |
28 | "table-column", | |
29 | "table-column-group", | |
30 | "table-caption", | |
31 | "table-cell" | |
32 | ); | |
33 | ||
34 | public static $ROW_GROUPS = array( | |
35 | 'table-row-group', | |
36 | 'table-header-group', | |
37 | 'table-footer-group' | |
38 | ); | |
39 | ||
40 | /** | |
41 | * The Cellmap object for this table. The cellmap maps table cells | |
42 | * to rows and columns, and aids in calculating column widths. | |
43 | * | |
44 | * @var \Dompdf\Cellmap | |
45 | */ | |
46 | protected $_cellmap; | |
47 | ||
48 | /** | |
49 | * The minimum width of the table, in pt | |
50 | * | |
51 | * @var float | |
52 | */ | |
53 | protected $_min_width; | |
54 | ||
55 | /** | |
56 | * The maximum width of the table, in pt | |
57 | * | |
58 | * @var float | |
59 | */ | |
60 | protected $_max_width; | |
61 | ||
62 | /** | |
63 | * Table header rows. Each table header is duplicated when a table | |
64 | * spans pages. | |
65 | * | |
66 | * @var array | |
67 | */ | |
68 | protected $_headers; | |
69 | ||
70 | /** | |
71 | * Table footer rows. Each table footer is duplicated when a table | |
72 | * spans pages. | |
73 | * | |
74 | * @var array | |
75 | */ | |
76 | protected $_footers; | |
77 | ||
78 | /** | |
79 | * Class constructor | |
80 | * | |
81 | * @param Frame $frame the frame to decorate | |
82 | * @param Dompdf $dompdf | |
83 | */ | |
84 | public function __construct(Frame $frame, Dompdf $dompdf) | |
85 | { | |
86 | parent::__construct($frame, $dompdf); | |
87 | $this->_cellmap = new Cellmap($this); | |
88 | ||
89 | if ($frame->get_style()->table_layout === "fixed") { | |
90 | $this->_cellmap->set_layout_fixed(true); | |
91 | } | |
92 | ||
93 | $this->_min_width = null; | |
94 | $this->_max_width = null; | |
95 | $this->_headers = array(); | |
96 | $this->_footers = array(); | |
97 | } | |
98 | ||
99 | ||
100 | public function reset() | |
101 | { | |
102 | parent::reset(); | |
103 | $this->_cellmap->reset(); | |
104 | $this->_min_width = null; | |
105 | $this->_max_width = null; | |
106 | $this->_headers = array(); | |
107 | $this->_footers = array(); | |
108 | $this->_reflower->reset(); | |
109 | } | |
110 | ||
111 | //........................................................................ | |
112 | ||
113 | /** | |
114 | * split the table at $row. $row and all subsequent rows will be | |
115 | * added to the clone. This method is overidden in order to remove | |
116 | * frames from the cellmap properly. | |
117 | * | |
118 | * @param Frame $child | |
119 | * @param bool $force_pagebreak | |
120 | * | |
121 | * @return void | |
122 | */ | |
123 | public function split(Frame $child = null, $force_pagebreak = false) | |
124 | { | |
125 | if (is_null($child)) { | |
126 | parent::split(); | |
127 | ||
128 | return; | |
129 | } | |
130 | ||
131 | // If $child is a header or if it is the first non-header row, do | |
132 | // not duplicate headers, simply move the table to the next page. | |
133 | if (count($this->_headers) && !in_array($child, $this->_headers, true) && | |
134 | !in_array($child->get_prev_sibling(), $this->_headers, true) | |
135 | ) { | |
136 | ||
137 | $first_header = null; | |
138 | ||
139 | // Insert copies of the table headers before $child | |
140 | foreach ($this->_headers as $header) { | |
141 | ||
142 | $new_header = $header->deep_copy(); | |
143 | ||
144 | if (is_null($first_header)) { | |
145 | $first_header = $new_header; | |
146 | } | |
147 | ||
148 | $this->insert_child_before($new_header, $child); | |
149 | } | |
150 | ||
151 | parent::split($first_header); | |
152 | ||
153 | } elseif (in_array($child->get_style()->display, self::$ROW_GROUPS)) { | |
154 | ||
155 | // Individual rows should have already been handled | |
156 | parent::split($child); | |
157 | ||
158 | } else { | |
159 | ||
160 | $iter = $child; | |
161 | ||
162 | while ($iter) { | |
163 | $this->_cellmap->remove_row($iter); | |
164 | $iter = $iter->get_next_sibling(); | |
165 | } | |
166 | ||
167 | parent::split($child); | |
168 | } | |
169 | } | |
170 | ||
171 | /** | |
172 | * Return a copy of this frame with $node as its node | |
173 | * | |
174 | * @param DOMNode $node | |
175 | * | |
176 | * @return Frame | |
177 | */ | |
178 | public function copy(DOMNode $node) | |
179 | { | |
180 | $deco = parent::copy($node); | |
181 | ||
182 | // In order to keep columns' widths through pages | |
183 | $deco->_cellmap->set_columns($this->_cellmap->get_columns()); | |
184 | $deco->_cellmap->lock_columns(); | |
185 | ||
186 | return $deco; | |
187 | } | |
188 | ||
189 | /** | |
190 | * Static function to locate the parent table of a frame | |
191 | * | |
192 | * @param Frame $frame | |
193 | * | |
194 | * @return Table the table that is an ancestor of $frame | |
195 | */ | |
196 | public static function find_parent_table(Frame $frame) | |
197 | { | |
198 | ||
199 | while ($frame = $frame->get_parent()) | |
200 | if ($frame->is_table()) | |
201 | break; | |
202 | ||
203 | return $frame; | |
204 | } | |
205 | ||
206 | /** | |
207 | * Return this table's Cellmap | |
208 | * | |
209 | * @return \Dompdf\Cellmap | |
210 | */ | |
211 | public function get_cellmap() | |
212 | { | |
213 | return $this->_cellmap; | |
214 | } | |
215 | ||
216 | /** | |
217 | * Return the minimum width of this table | |
218 | * | |
219 | * @return float | |
220 | */ | |
221 | public function get_min_width() | |
222 | { | |
223 | return $this->_min_width; | |
224 | } | |
225 | ||
226 | /** | |
227 | * Return the maximum width of this table | |
228 | * | |
229 | * @return float | |
230 | */ | |
231 | public function get_max_width() | |
232 | { | |
233 | return $this->_max_width; | |
234 | } | |
235 | ||
236 | /** | |
237 | * Set the minimum width of the table | |
238 | * | |
239 | * @param float $width the new minimum width | |
240 | */ | |
241 | public function set_min_width($width) | |
242 | { | |
243 | $this->_min_width = $width; | |
244 | } | |
245 | ||
246 | /** | |
247 | * Set the maximum width of the table | |
248 | * | |
249 | * @param float $width the new maximum width | |
250 | */ | |
251 | public function set_max_width($width) | |
252 | { | |
253 | $this->_max_width = $width; | |
254 | } | |
255 | ||
256 | /** | |
257 | * Restructure tree so that the table has the correct structure. | |
258 | * Invalid children (i.e. all non-table-rows) are moved below the | |
259 | * table. | |
260 | */ | |
261 | public function normalise() | |
262 | { | |
263 | // Store frames generated by invalid tags and move them outside the table | |
264 | $erroneous_frames = array(); | |
265 | $anon_row = false; | |
266 | $iter = $this->get_first_child(); | |
267 | while ($iter) { | |
268 | $child = $iter; | |
269 | $iter = $iter->get_next_sibling(); | |
270 | ||
271 | $display = $child->get_style()->display; | |
272 | ||
273 | if ($anon_row) { | |
274 | ||
275 | if ($display === "table-row") { | |
276 | // Add the previous anonymous row | |
277 | $this->insert_child_before($table_row, $child); | |
278 | ||
279 | $table_row->normalise(); | |
280 | $child->normalise(); | |
281 | $anon_row = false; | |
282 | continue; | |
283 | } | |
284 | ||
285 | // add the child to the anonymous row | |
286 | $table_row->append_child($child); | |
287 | continue; | |
288 | ||
289 | } else { | |
290 | ||
291 | if ($display === "table-row") { | |
292 | $child->normalise(); | |
293 | continue; | |
294 | } | |
295 | ||
296 | if ($display === "table-cell") { | |
297 | // Create an anonymous table row | |
298 | $tr = $this->get_node()->ownerDocument->createElement("tr"); | |
299 | ||
300 | $frame = new Frame($tr); | |
301 | ||
302 | $css = $this->get_style()->get_stylesheet(); | |
303 | $style = $css->create_style(); | |
304 | $style->inherit($this->get_style()); | |
305 | ||
306 | // Lookup styles for tr tags. If the user wants styles to work | |
307 | // better, they should make the tr explicit... I'm not going to | |
308 | // try to guess what they intended. | |
309 | if ($tr_style = $css->lookup("tr")) { | |
310 | $style->merge($tr_style); | |
311 | } | |
312 | ||
313 | // Okay, I have absolutely no idea why I need this clone here, but | |
314 | // if it's omitted, php (as of 2004-07-28) segfaults. | |
315 | $frame->set_style(clone $style); | |
316 | $table_row = Factory::decorate_frame($frame, $this->_dompdf, $this->_root); | |
317 | ||
318 | // Add the cell to the row | |
319 | $table_row->append_child($child); | |
320 | ||
321 | $anon_row = true; | |
322 | continue; | |
323 | } | |
324 | ||
325 | if (!in_array($display, self::$VALID_CHILDREN)) { | |
326 | $erroneous_frames[] = $child; | |
327 | continue; | |
328 | } | |
329 | ||
330 | // Normalise other table parts (i.e. row groups) | |
331 | foreach ($child->get_children() as $grandchild) { | |
332 | if ($grandchild->get_style()->display === "table-row") { | |
333 | $grandchild->normalise(); | |
334 | } | |
335 | } | |
336 | ||
337 | // Add headers and footers | |
338 | if ($display === "table-header-group") { | |
339 | $this->_headers[] = $child; | |
340 | } elseif ($display === "table-footer-group") { | |
341 | $this->_footers[] = $child; | |
342 | } | |
343 | } | |
344 | } | |
345 | ||
346 | if ($anon_row && $table_row instanceof DOMNode) { | |
347 | // Add the row to the table | |
348 | $this->_frame->append_child($table_row); | |
349 | $table_row->normalise(); | |
350 | $this->_cellmap->add_row(); | |
351 | } | |
352 | ||
353 | foreach ($erroneous_frames as $frame) { | |
354 | $this->move_after($frame); | |
355 | } | |
356 | } | |
357 | ||
358 | //........................................................................ | |
359 | ||
360 | /** | |
361 | * Moves the specified frame and it's corresponding node outside of | |
362 | * the table. | |
363 | * | |
364 | * @param Frame $frame the frame to move | |
365 | */ | |
366 | public function move_after(Frame $frame) | |
367 | { | |
368 | $this->get_parent()->insert_child_after($frame, $this); | |
369 | } | |
370 | } |