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