View Javadoc
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 <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
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 rows 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 &lt;= index &lt;= 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 }