View Javadoc
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 &lt;= index &lt;= 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 }