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 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  @JsxClass
54  public class HTMLCollection extends AbstractList {
55  
56      /**
57       * Creates an instance.
58       */
59      public HTMLCollection() {
60          super();
61      }
62  
63      /**
64       * JavaScript constructor.
65       */
66      @JsxConstructor
67      public void jsConstructor() {
68          // nothing to do
69      }
70  
71      /**
72       * Creates an instance.
73       * @param domNode parent scope
74       * @param attributeChangeSensitive indicates if the content of the collection may change when an attribute
75       *        of a descendant node of parentScope changes (attribute added, modified or removed)
76       */
77      public HTMLCollection(final DomNode domNode, final boolean attributeChangeSensitive) {
78          super(domNode, attributeChangeSensitive, null);
79      }
80  
81      /**
82       * Constructs an instance with an initial cache value.
83       * @param domNode the parent scope, on which we listen for changes
84       * @param initialElements the initial content for the cache
85       */
86      HTMLCollection(final DomNode domNode, final List<DomNode> initialElements) {
87          super(domNode, true, new ArrayList<>(initialElements));
88      }
89  
90      private HTMLCollection(final DomNode domNode, final boolean attributeChangeSensitive,
91              final List<DomNode> initialElements) {
92          super(domNode, attributeChangeSensitive, new ArrayList<>(initialElements));
93      }
94  
95      /**
96       * Gets an empty collection.
97       * @param domNode the DOM node
98       * @return an empty collection
99       */
100     public static HTMLCollection emptyCollection(final DomNode domNode) {
101         return new HTMLCollection(domNode, false, Collections.emptyList());
102     }
103 
104     /**
105      * {@inheritDoc}
106      */
107     @Override
108     protected HTMLCollection create(final DomNode parentScope, final List<DomNode> initialElements) {
109         return new HTMLCollection(parentScope, initialElements);
110     }
111 
112     /**
113      * @return the Iterator symbol
114      */
115     @JsxSymbol
116     public Scriptable iterator() {
117         return JavaScriptEngine.newArrayIteratorTypeValues(getParentScope(), this);
118     }
119 
120     /**
121      * Returns the length.
122      * @return the length
123      */
124     @JsxGetter
125     @Override
126     public final int getLength() {
127         return super.getLength();
128     }
129 
130     /**
131      * {@inheritDoc}
132      */
133     @Override
134     protected Object getWithPreemptionByName(final String name, final List<DomNode> elements) {
135         final List<DomNode> matchingElements = new ArrayList<>();
136         final boolean searchName = isGetWithPreemptionSearchName();
137         for (final DomNode next : elements) {
138             if (next instanceof DomElement
139                     && (searchName || next instanceof HtmlInput || next instanceof HtmlForm)) {
140                 final String nodeName = ((DomElement) next).getAttributeDirect(DomElement.NAME_ATTRIBUTE);
141                 if (name.equals(nodeName)) {
142                     matchingElements.add(next);
143                 }
144             }
145         }
146 
147         if (matchingElements.isEmpty()) {
148             return NOT_FOUND;
149         }
150 
151         if (matchingElements.size() == 1) {
152             return getScriptableForElement(matchingElements.get(0));
153         }
154 
155         // many elements => build a sub collection
156         final DomNode domNode = getDomNodeOrNull();
157         final HTMLCollection collection = new HTMLCollection(domNode, matchingElements);
158         collection.setAvoidObjectDetection(true);
159         return collection;
160     }
161 
162     /**
163      * Returns whether {@link #getWithPreemption(String)} should search by name or not.
164      * @return whether {@link #getWithPreemption(String)} should search by name or not
165      */
166     protected boolean isGetWithPreemptionSearchName() {
167         return true;
168     }
169 
170     /**
171      * Returns the item or items corresponding to the specified index or key.
172      * @param index the index or key corresponding to the element or elements to return
173      * @return the element or elements corresponding to the specified index or key
174      * @see <a href="http://msdn.microsoft.com/en-us/library/ms536460.aspx">MSDN doc</a>
175      */
176     @JsxFunction
177     public Object item(final Object index) {
178         int idx = 0;
179         final double doubleValue = JavaScriptEngine.toNumber(index);
180         if (!Double.isNaN(doubleValue)) {
181             idx = (int) doubleValue;
182         }
183 
184         final Object object = get(idx, this);
185         if (object == NOT_FOUND) {
186             return null;
187         }
188         return object;
189     }
190 
191     /**
192      * Retrieves the item or items corresponding to the specified name (checks ids, and if
193      * that does not work, then names).
194      * @param name the name or id the element or elements to return
195      * @return the element or elements corresponding to the specified name or id
196      * @see <a href="http://msdn.microsoft.com/en-us/library/ms536634.aspx">MSDN doc</a>
197      */
198     @JsxFunction
199     public Scriptable namedItem(final String name) {
200         final List<DomNode> elements = getElements();
201         final BrowserVersion browserVersion = getBrowserVersion();
202         if (browserVersion.hasFeature(HTMLCOLLECTION_NAMED_ITEM_ID_FIRST)) {
203             for (final Object next : elements) {
204                 if (next instanceof DomElement) {
205                     final DomElement elem = (DomElement) next;
206                     final String id = elem.getId();
207                     if (name.equals(id)) {
208                         return getScriptableForElement(elem);
209                     }
210                 }
211             }
212         }
213         for (final Object next : elements) {
214             if (next instanceof DomElement) {
215                 final DomElement elem = (DomElement) next;
216                 final String nodeName = elem.getAttributeDirect(DomElement.NAME_ATTRIBUTE);
217                 if (name.equals(nodeName)) {
218                     return getScriptableForElement(elem);
219                 }
220 
221                 final String id = elem.getId();
222                 if (name.equals(id)) {
223                     return getScriptableForElement(elem);
224                 }
225             }
226         }
227         return null;
228     }
229 }