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