1 /*
2 * Copyright (c) 2002-2026 Gargoyle Software Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 * https://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15 package org.htmlunit.javascript.host.html;
16
17 import java.io.Serializable;
18 import java.util.ArrayList;
19 import java.util.List;
20 import java.util.function.Predicate;
21 import java.util.function.Supplier;
22
23 import org.htmlunit.html.DomElement;
24 import org.htmlunit.html.DomNode;
25 import org.htmlunit.html.HtmlElement;
26 import org.htmlunit.html.HtmlPage;
27 import org.htmlunit.html.HtmlTable;
28 import org.htmlunit.html.HtmlTableBody;
29 import org.htmlunit.html.HtmlTableFooter;
30 import org.htmlunit.html.HtmlTableHeader;
31 import org.htmlunit.html.HtmlTableRow;
32 import org.htmlunit.javascript.HtmlUnitScriptable;
33 import org.htmlunit.javascript.JavaScriptEngine;
34 import org.htmlunit.javascript.configuration.JsxClass;
35 import org.htmlunit.javascript.configuration.JsxConstructor;
36 import org.htmlunit.javascript.configuration.JsxFunction;
37 import org.htmlunit.javascript.configuration.JsxGetter;
38 import org.htmlunit.javascript.configuration.JsxSetter;
39 import org.htmlunit.javascript.host.dom.DOMException;
40 import org.htmlunit.javascript.host.dom.Node;
41
42 /**
43 * The JavaScript object {@code HTMLTableElement}.
44 *
45 * @author David D. Kilzer
46 * @author Mike Bowler
47 * @author Daniel Gredler
48 * @author Chris Erskine
49 * @author Marc Guillemot
50 * @author Ahmed Ashour
51 * @author Ronald Brill
52 * @author Frank Danek
53 */
54 @JsxClass(domClass = HtmlTable.class)
55 public class HTMLTableElement extends HTMLElement {
56
57 /**
58 * JavaScript constructor.
59 */
60 @Override
61 @JsxConstructor
62 public void jsConstructor() {
63 super.jsConstructor();
64 }
65
66 /**
67 * Returns the table's caption element, or {@code null} if none exists. If more than one
68 * caption is declared in the table, this method returns the first one.
69 * @return the table's caption element
70 */
71 @JsxGetter
72 public HtmlUnitScriptable getCaption() {
73 final List<HtmlElement> captions = getDomNodeOrDie().getStaticElementsByTagName("caption");
74 if (captions.isEmpty()) {
75 return null;
76 }
77 return getScriptableFor(captions.get(0));
78 }
79
80 /**
81 * Sets the caption.
82 * @param o the caption
83 */
84 @JsxSetter
85 public void setCaption(final Object o) {
86 if (!(o instanceof HTMLTableCaptionElement caption)) {
87 throw JavaScriptEngine.typeError("Not a caption");
88 }
89
90 // remove old caption (if any)
91 deleteCaption();
92
93 getDomNodeOrDie().appendChild(caption.getDomNodeOrDie());
94 }
95
96 /**
97 * Returns the table's tfoot element, or {@code null} if none exists. If more than one
98 * tfoot is declared in the table, this method returns the first one.
99 * @return the table's tfoot element
100 */
101 @JsxGetter
102 public HtmlUnitScriptable getTFoot() {
103 final List<HtmlElement> tfoots = getDomNodeOrDie().getStaticElementsByTagName("tfoot");
104 if (tfoots.isEmpty()) {
105 return null;
106 }
107 return getScriptableFor(tfoots.get(0));
108 }
109
110 /**
111 * Sets the tFoot.
112 * @param o the tFoot
113 */
114 @JsxSetter
115 public void setTFoot(final Object o) {
116 if (!(o instanceof HTMLTableSectionElement element
117 && "TFOOT".equals(element.getTagName()))) {
118 throw JavaScriptEngine.typeError("Not a tFoot");
119 }
120
121 // remove old caption (if any)
122 deleteTFoot();
123
124 getDomNodeOrDie().appendChild(element.getDomNodeOrDie());
125 }
126
127 /**
128 * Returns the table's thead element, or {@code null} if none exists. If more than one
129 * thead is declared in the table, this method returns the first one.
130 * @return the table's thead element
131 */
132 @JsxGetter
133 public HtmlUnitScriptable getTHead() {
134 final List<HtmlElement> theads = getDomNodeOrDie().getStaticElementsByTagName("thead");
135 if (theads.isEmpty()) {
136 return null;
137 }
138 return getScriptableFor(theads.get(0));
139 }
140
141 /**
142 * Sets the {@code tHead}.
143 * @param o the {@code tHead}
144 */
145 @JsxSetter
146 public void setTHead(final Object o) {
147 if (!(o instanceof HTMLTableSectionElement element
148 && "THEAD".equals(element.getTagName()))) {
149 throw JavaScriptEngine.typeError("Not a tHead");
150 }
151
152 // remove old caption (if any)
153 deleteTHead();
154
155 getDomNodeOrDie().appendChild(element.getDomNodeOrDie());
156 }
157
158 /**
159 * Returns the tbody's in the table.
160 * @return the tbody's in the table
161 */
162 @JsxGetter
163 public HtmlUnitScriptable getTBodies() {
164 final HtmlTable table = (HtmlTable) getDomNodeOrDie();
165 final HTMLCollection bodies = new HTMLCollection(table, false);
166 bodies.setElementsSupplier((Supplier<List<DomNode>> & Serializable) () -> new ArrayList<>(table.getBodies()));
167 return bodies;
168 }
169
170 /**
171 * If this table does not have a caption, this method creates an empty table caption,
172 * adds it to the table and then returns it. If one or more captions already exist,
173 * this method returns the first existing caption.
174 * @see <a href="http://msdn.microsoft.com/en-us/library/ms536381.aspx">MSDN Documentation</a>
175 * @return a newly added caption if no caption exists, or the first existing caption
176 */
177 @JsxFunction
178 public HtmlUnitScriptable createCaption() {
179 return getScriptableFor(getDomNodeOrDie().appendChildIfNoneExists("caption"));
180 }
181
182 /**
183 * If this table does not have a tfoot element, this method creates an empty tfoot
184 * element, adds it to the table and then returns it. If this table already has a
185 * tfoot element, this method returns the existing tfoot element.
186 * @see <a href="http://msdn.microsoft.com/en-us/library/ms536402.aspx">MSDN Documentation</a>
187 * @return a newly added caption if no caption exists, or the first existing caption
188 */
189 @JsxFunction
190 public HtmlUnitScriptable createTFoot() {
191 return getScriptableFor(getDomNodeOrDie().appendChildIfNoneExists("tfoot"));
192 }
193
194 /**
195 * If this table does not have a tbody element, this method creates an empty tbody
196 * element, adds it to the table and then returns it. If this table already has a
197 * tbody element, this method returns the existing tbody element.
198 * @see <a href="http://msdn.microsoft.com/en-us/library/ms536402.aspx">MSDN Documentation</a>
199 * @return a newly added caption if no caption exists, or the first existing caption
200 */
201 @JsxFunction
202 public HtmlUnitScriptable createTBody() {
203 return getScriptableFor(getDomNodeOrDie().appendChildIfNoneExists("tbody"));
204 }
205
206 /**
207 * If this table does not have a thead element, this method creates an empty
208 * thead element, adds it to the table and then returns it. If this table
209 * already has a thead element, this method returns the existing thead element.
210 * @see <a href="http://msdn.microsoft.com/en-us/library/ms536403.aspx">MSDN Documentation</a>
211 * @return a newly added caption if no caption exists, or the first existing caption
212 */
213 @JsxFunction
214 public HtmlUnitScriptable createTHead() {
215 return getScriptableFor(getDomNodeOrDie().appendChildIfNoneExists("thead"));
216 }
217
218 /**
219 * Deletes this table's caption. If the table has multiple captions, this method
220 * deletes only the first caption. If this table does not have any captions, this
221 * method does nothing.
222 * @see <a href="http://msdn.microsoft.com/en-us/library/ms536405.aspx">MSDN Documentation</a>
223 */
224 @JsxFunction
225 public void deleteCaption() {
226 getDomNodeOrDie().removeChild("caption", 0);
227 }
228
229 /**
230 * Deletes this table's tfoot element. If the table has multiple tfoot elements, this
231 * method deletes only the first tfoot element. If this table does not have any tfoot
232 * elements, this method does nothing.
233 * @see <a href="http://msdn.microsoft.com/en-us/library/ms536409.aspx">MSDN Documentation</a>
234 */
235 @JsxFunction
236 public void deleteTFoot() {
237 getDomNodeOrDie().removeChild("tfoot", 0);
238 }
239
240 /**
241 * Deletes this table's thead element. If the table has multiple thead elements, this
242 * method deletes only the first thead element. If this table does not have any thead
243 * elements, this method does nothing.
244 * @see <a href="http://msdn.microsoft.com/en-us/library/ms536410.aspx">MSDN Documentation</a>
245 */
246 @JsxFunction
247 public void deleteTHead() {
248 getDomNodeOrDie().removeChild("thead", 0);
249 }
250
251 /**
252 * Inserts a new row at the specified index in the element's row collection. If the index
253 * is -1 or there is no index specified, then the row is appended at the end of the
254 * element's row collection.
255 * @see <a href="http://msdn.microsoft.com/en-us/library/ms536457.aspx">MSDN Documentation</a>
256 * @param index specifies where to insert the row in the row's collection.
257 * The default value is -1, which appends the new row to the end of the rows collection
258 * @return the newly-created row
259 */
260 @JsxFunction
261 public HtmlUnitScriptable insertRow(final Object index) {
262 int rowIndex = -1;
263 if (!JavaScriptEngine.isUndefined(index)) {
264 rowIndex = (int) JavaScriptEngine.toNumber(index);
265 }
266 final HTMLCollection rows = getRows();
267 final int rowCount = rows.getLength();
268 final int r;
269 if (rowIndex == -1 || rowIndex == rowCount) {
270 r = Math.max(0, rowCount);
271 }
272 else {
273 r = rowIndex;
274 }
275
276 if (r < 0 || r > rowCount) {
277 throw JavaScriptEngine.asJavaScriptException(
278 getWindow(),
279 "Index or size is negative or greater than the allowed amount "
280 + "(index: " + rowIndex + ", " + rowCount + " rows)",
281 DOMException.INDEX_SIZE_ERR);
282 }
283
284 return insertRow(r);
285 }
286
287 /**
288 * Inserts a new row at the given position.
289 * @param index the index where the row should be inserted (0 <= index <= nbRows)
290 * @return the inserted row
291 */
292 public HtmlUnitScriptable insertRow(final int index) {
293 // check if a tbody should be created
294 if (index != 0) {
295 for (final HtmlElement htmlElement : getDomNodeOrDie().getHtmlElementDescendants()) {
296 if (htmlElement instanceof HtmlTableBody
297 || htmlElement instanceof HtmlTableHeader
298 || htmlElement instanceof HtmlTableFooter) {
299
300 final HTMLCollection rows = getRows();
301 final int rowCount = rows.getLength();
302 final DomElement newRow = ((HtmlPage) getDomNodeOrDie().getPage()).createElement("tr");
303 if (rowCount == 0) {
304 getDomNodeOrDie().appendChild(newRow);
305 }
306 else if (index == rowCount) {
307 final HtmlUnitScriptable row = (HtmlUnitScriptable) rows.item(Integer.valueOf(index - 1));
308 row.getDomNodeOrDie().getParentNode().appendChild(newRow);
309 }
310 else {
311 final HtmlUnitScriptable row = (HtmlUnitScriptable) rows.item(Integer.valueOf(index));
312 // if at the end, then in the same "sub-container" as the last existing row
313 if (index > rowCount - 1) {
314 row.getDomNodeOrDie().getParentNode().appendChild(newRow);
315 }
316 else {
317 row.getDomNodeOrDie().insertBefore(newRow);
318 }
319 }
320 return getScriptableFor(newRow);
321 }
322 }
323 }
324
325 final HtmlElement tBody = getDomNodeOrDie().appendChildIfNoneExists("tbody");
326 return ((HTMLTableSectionElement) getScriptableFor(tBody)).insertRow(0);
327 }
328
329 /**
330 * Returns the {@code width} property.
331 * @return the {@code width} property
332 */
333 @JsxGetter(propertyName = "width")
334 public String getWidth_js() {
335 return getDomNodeOrDie().getAttributeDirect("width");
336 }
337
338 /**
339 * Sets the {@code width} property.
340 * @param width the {@code width} property
341 */
342 @JsxSetter(propertyName = "width")
343 public void setWidth_js(final String width) {
344 getDomNodeOrDie().setAttribute("width", width);
345 }
346
347 /**
348 * Returns the {@code cellSpacing} property.
349 * @return the {@code cellSpacing} property
350 */
351 @JsxGetter
352 public String getCellSpacing() {
353 return getDomNodeOrDie().getAttributeDirect("cellspacing");
354 }
355
356 /**
357 * Sets the {@code cellSpacing} property.
358 * @param cellSpacing the {@code cellSpacing} property
359 */
360 @JsxSetter
361 public void setCellSpacing(final String cellSpacing) {
362 getDomNodeOrDie().setAttribute("cellspacing", cellSpacing);
363 }
364
365 /**
366 * Returns the {@code cellPadding} property.
367 * @return the {@code cellPadding} property
368 */
369 @JsxGetter
370 public String getCellPadding() {
371 return getDomNodeOrDie().getAttributeDirect("cellpadding");
372 }
373
374 /**
375 * Sets the {@code cellPadding} property.
376 * @param cellPadding the {@code cellPadding} property
377 */
378 @JsxSetter
379 public void setCellPadding(final String cellPadding) {
380 getDomNodeOrDie().setAttribute("cellpadding", cellPadding);
381 }
382
383 /**
384 * Gets the {@code border} property.
385 * @return the {@code border} property
386 */
387 @JsxGetter
388 public String getBorder() {
389 return getDomNodeOrDie().getAttributeDirect("border");
390 }
391
392 /**
393 * Sets the {@code border} property.
394 * @param border the {@code border} property
395 */
396 @JsxSetter
397 public void setBorder(final String border) {
398 getDomNodeOrDie().setAttribute("border", border);
399 }
400
401 /**
402 * Returns the value of the {@code bgColor} property.
403 * @return the value of the {@code bgColor} property
404 * @see <a href="http://msdn.microsoft.com/en-us/library/ms533505.aspx">MSDN Documentation</a>
405 */
406 @JsxGetter
407 public String getBgColor() {
408 return getDomNodeOrDie().getAttribute("bgColor");
409 }
410
411 /**
412 * Sets the value of the {@code bgColor} property.
413 * @param bgColor the value of the {@code bgColor} property
414 * @see <a href="http://msdn.microsoft.com/en-us/library/ms533505.aspx">MSDN Documentation</a>
415 */
416 @JsxSetter
417 public void setBgColor(final String bgColor) {
418 setColorAttribute("bgColor", bgColor);
419 }
420
421 /**
422 * {@inheritDoc}
423 */
424 @Override
425 public Node appendChild(final Object childObject) {
426 final Node appendedChild = super.appendChild(childObject);
427 getDomNodeOrDie().getPage().clearComputedStyles(getDomNodeOrDie());
428 return appendedChild;
429 }
430
431 /**
432 * {@inheritDoc}
433 */
434 @Override
435 public Node removeChild(final Object childObject) {
436 final Node removedChild = super.removeChild(childObject);
437 getDomNodeOrDie().getPage().clearComputedStyles(getDomNodeOrDie());
438 return removedChild;
439 }
440
441 /**
442 * Gets the {@code summary} property.
443 * @return the property
444 */
445 @JsxGetter
446 public String getSummary() {
447 return getDomNodeOrDie().getAttributeDirect("summary");
448 }
449
450 /**
451 * Sets the {@code summary} property.
452 * @param summary the new property
453 */
454 @JsxSetter
455 public void setSummary(final String summary) {
456 setAttribute("summary", summary);
457 }
458
459 /**
460 * Gets the {@code rules} property.
461 * @return the property
462 */
463 @JsxGetter
464 public String getRules() {
465 return getDomNodeOrDie().getAttributeDirect("rules");
466 }
467
468 /**
469 * Sets the {@code rules} property.
470 * @param rules the new property
471 */
472 @JsxSetter
473 public void setRules(final String rules) {
474 setAttribute("rules", rules);
475 }
476
477 /**
478 * Returns the value of the {@code align} property.
479 * @return the value of the {@code align} property
480 */
481 @JsxGetter
482 public String getAlign() {
483 return getAlign(true);
484 }
485
486 /**
487 * Sets the value of the {@code align} property.
488 * @param align the value of the {@code align} property
489 */
490 @JsxSetter
491 public void setAlign(final String align) {
492 setAlign(align, false);
493 }
494
495 /**
496 * Deletes the row at the specified index.
497 * @see <a href="http://msdn.microsoft.com/en-us/library/ms536408.aspx">MSDN Documentation</a>
498 * @param rowIndex the zero-based index of the row to delete
499 */
500 @JsxFunction
501 public void deleteRow(int rowIndex) {
502 final HTMLCollection rows = getRows();
503 final int rowCount = rows.getLength();
504 if (rowIndex == -1) {
505 rowIndex = rowCount - 1;
506 }
507 final boolean rowIndexValid = rowIndex >= 0 && rowIndex < rowCount;
508 if (rowIndexValid) {
509 final HtmlUnitScriptable row = (HtmlUnitScriptable) rows.item(Integer.valueOf(rowIndex));
510 row.getDomNodeOrDie().remove();
511 }
512 }
513
514 /**
515 * Returns the rows in the element.
516 * @return the rows in the element
517 */
518 @JsxGetter
519 public HTMLCollection getRows() {
520 final HTMLCollection rows = new HTMLCollection(getDomNodeOrDie(), false);
521 rows.setIsMatchingPredicate(
522 (Predicate<DomNode> & Serializable)
523 node -> node instanceof HtmlTableRow htr && isContainedRow(htr));
524 return rows;
525 }
526
527 /**
528 * Indicates if the row belongs to this container.
529 * @param row the row to test
530 * @return {@code true} if it belongs to this container
531 */
532 private boolean isContainedRow(final HtmlTableRow row) {
533 final DomNode parent = row.getParentNode(); // the tbody, thead or tfoo
534 return parent != null
535 && parent.getParentNode() == getDomNodeOrDie();
536 }
537 }