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