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 static org.htmlunit.BrowserVersionFeatures.HTMLCOLLECTION_NAMED_ITEM_ID_FIRST;
18  
19  import java.util.ArrayList;
20  import java.util.Collections;
21  import java.util.List;
22  
23  import org.htmlunit.BrowserVersion;
24  import org.htmlunit.corejs.javascript.Scriptable;
25  import org.htmlunit.html.DomElement;
26  import org.htmlunit.html.DomNode;
27  import org.htmlunit.html.HtmlForm;
28  import org.htmlunit.html.HtmlInput;
29  import org.htmlunit.javascript.JavaScriptEngine;
30  import org.htmlunit.javascript.configuration.JsxClass;
31  import org.htmlunit.javascript.configuration.JsxConstructor;
32  import org.htmlunit.javascript.configuration.JsxFunction;
33  import org.htmlunit.javascript.configuration.JsxGetter;
34  import org.htmlunit.javascript.configuration.JsxSymbol;
35  import org.htmlunit.javascript.host.dom.AbstractList;
36  
37  /**
38   * An array of elements. Used for the element arrays returned by <code>document.all</code>,
39   * <code>document.all.tags('x')</code>, <code>document.forms</code>, <code>window.frames</code>, etc.
40   * Note that this class must not be used for collections that can be modified, for example
41   * <code>map.areas</code> and <code>select.options</code>.
42   * <br>
43   * This class (like all classes in this package) is specific for the JavaScript engine.
44   * Users of HtmlUnit shouldn't use it directly.
45   *
46   * @author Daniel Gredler
47   * @author Marc Guillemot
48   * @author Chris Erskine
49   * @author Ahmed Ashour
50   * @author Frank Danek
51   * @author Ronald Brill
52   *
53   * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLCollection">MDN Documentation</a>
54   */
55  @JsxClass
56  public class HTMLCollection extends AbstractList {
57  
58      /**
59       * Creates an instance.
60       */
61      public HTMLCollection() {
62          super();
63      }
64  
65      /**
66       * JavaScript constructor.
67       */
68      @JsxConstructor
69      public void jsConstructor() {
70          // nothing to do
71      }
72  
73      /**
74       * Creates an instance.
75       * @param domNode the parent scope
76       * @param attributeChangeSensitive indicates if the content of the collection may change when an attribute
77       *        of a descendant node of domNode changes (attribute added, modified or removed)
78       */
79      public HTMLCollection(final DomNode domNode, final boolean attributeChangeSensitive) {
80          super(domNode, attributeChangeSensitive, null);
81      }
82  
83      /**
84       * Constructs an instance with an initial cache value.
85       * @param domNode the parent scope, on which we listen for changes
86       * @param initialElements the initial content for the cache
87       */
88      HTMLCollection(final DomNode domNode, final List<DomNode> initialElements) {
89          super(domNode, true, new ArrayList<>(initialElements));
90      }
91  
92      private HTMLCollection(final DomNode domNode, final boolean attributeChangeSensitive,
93              final List<DomNode> initialElements) {
94          super(domNode, attributeChangeSensitive, new ArrayList<>(initialElements));
95      }
96  
97      /**
98       * Gets an empty collection.
99       * @param domNode the DOM node
100      * @return an empty collection
101      */
102     public static HTMLCollection emptyCollection(final DomNode domNode) {
103         return new HTMLCollection(domNode, false, Collections.emptyList());
104     }
105 
106     /**
107      * {@inheritDoc}
108      */
109     @Override
110     protected HTMLCollection create(final DomNode parentScope, final List<DomNode> initialElements) {
111         return new HTMLCollection(parentScope, initialElements);
112     }
113 
114     /**
115      * Returns the {@code Symbol.iterator} function that allows iterating over this collection.
116      * @return the Iterator symbol
117      */
118     @JsxSymbol
119     public Scriptable iterator() {
120         return JavaScriptEngine.newArrayIteratorTypeValues(getParentScope(), this);
121     }
122 
123     /**
124      * Returns the length.
125      * @return the length
126      */
127     @JsxGetter
128     @Override
129     public final int getLength() {
130         return super.getLength();
131     }
132 
133     /**
134      * {@inheritDoc}
135      */
136     @Override
137     protected Object getWithPreemptionByName(final String name, final List<DomNode> elements) {
138         final List<DomNode> matchingElements = new ArrayList<>();
139         final boolean searchName = isGetWithPreemptionSearchName();
140         for (final DomNode next : elements) {
141             if (next instanceof DomElement element
142                     && (searchName || next instanceof HtmlInput || next instanceof HtmlForm)) {
143                 final String nodeName = element.getAttributeDirect(DomElement.NAME_ATTRIBUTE);
144                 if (name.equals(nodeName)) {
145                     matchingElements.add(next);
146                 }
147             }
148         }
149 
150         if (matchingElements.isEmpty()) {
151             return NOT_FOUND;
152         }
153 
154         if (matchingElements.size() == 1) {
155             return getScriptableForElement(matchingElements.get(0));
156         }
157 
158         // many elements => build a sub collection
159         final DomNode domNode = getDomNodeOrNull();
160         final HTMLCollection collection = new HTMLCollection(domNode, matchingElements);
161         collection.setAvoidObjectDetection(true);
162         return collection;
163     }
164 
165     /**
166      * Returns whether {@link #getWithPreemption(String)} should search by name or not.
167      * @return whether {@link #getWithPreemption(String)} should search by name or not
168      */
169     protected boolean isGetWithPreemptionSearchName() {
170         return true;
171     }
172 
173     /**
174      * Returns the item or items corresponding to the specified index or key.
175      * @param index the index or key corresponding to the element or elements to return
176      * @return the element or elements corresponding to the specified index or key
177      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLCollection/item">MDN Documentation</a>
178      */
179     @JsxFunction
180     public Object item(final Object index) {
181         int idx = 0;
182         final double doubleValue = JavaScriptEngine.toNumber(index);
183         if (!Double.isNaN(doubleValue)) {
184             idx = (int) doubleValue;
185         }
186 
187         final Object object = get(idx, this);
188         if (object == NOT_FOUND) {
189             return null;
190         }
191         return object;
192     }
193 
194     /**
195      * Retrieves the item or items corresponding to the specified name (checks ids, and if
196      * that does not work, then names).
197      * @param name the name or id of the element or elements to return
198      * @return the element or elements corresponding to the specified name or id
199      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLCollection/namedItem">MDN Documentation</a>
200      */
201     @JsxFunction
202     public Scriptable namedItem(final String name) {
203         final List<DomNode> elements = getElements();
204         final BrowserVersion browserVersion = getBrowserVersion();
205         if (browserVersion.hasFeature(HTMLCOLLECTION_NAMED_ITEM_ID_FIRST)) {
206             for (final Object next : elements) {
207                 if (next instanceof DomElement elem) {
208                     final String id = elem.getId();
209                     if (name.equals(id)) {
210                         return getScriptableForElement(elem);
211                     }
212                 }
213             }
214         }
215         for (final Object next : elements) {
216             if (next instanceof DomElement elem) {
217                 final String nodeName = elem.getAttributeDirect(DomElement.NAME_ATTRIBUTE);
218                 if (name.equals(nodeName)) {
219                     return getScriptableForElement(elem);
220                 }
221 
222                 final String id = elem.getId();
223                 if (name.equals(id)) {
224                     return getScriptableForElement(elem);
225                 }
226             }
227         }
228         return null;
229     }
230 }