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.Callable;
25  import org.htmlunit.corejs.javascript.Context;
26  import org.htmlunit.corejs.javascript.Scriptable;
27  import org.htmlunit.html.DomElement;
28  import org.htmlunit.html.DomNode;
29  import org.htmlunit.html.HtmlForm;
30  import org.htmlunit.html.HtmlInput;
31  import org.htmlunit.javascript.JavaScriptEngine;
32  import org.htmlunit.javascript.configuration.JsxClass;
33  import org.htmlunit.javascript.configuration.JsxConstructor;
34  import org.htmlunit.javascript.configuration.JsxFunction;
35  import org.htmlunit.javascript.configuration.JsxGetter;
36  import org.htmlunit.javascript.configuration.JsxSymbol;
37  import org.htmlunit.javascript.host.dom.AbstractList;
38  
39  /**
40   * An array of elements. Used for the element arrays returned by <code>document.all</code>,
41   * <code>document.all.tags('x')</code>, <code>document.forms</code>, <code>window.frames</code>, etc.
42   * Note that this class must not be used for collections that can be modified, for example
43   * <code>map.areas</code> and <code>select.options</code>.
44   * <br>
45   * This class (like all classes in this package) is specific for the JavaScript engine.
46   * Users of HtmlUnit shouldn't use it directly.
47   *
48   * @author Daniel Gredler
49   * @author Marc Guillemot
50   * @author Chris Erskine
51   * @author Ahmed Ashour
52   * @author Frank Danek
53   * @author Ronald Brill
54   */
55  @JsxClass
56  public class HTMLCollection extends AbstractList implements Callable {
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 parent scope
76       * @param attributeChangeSensitive indicates if the content of the collection may change when an attribute
77       *        of a descendant node of parentScope 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      * @return the Iterator symbol
116      */
117     @JsxSymbol
118     public Scriptable iterator() {
119         return JavaScriptEngine.newArrayIteratorTypeValues(getParentScope(), this);
120     }
121 
122     /**
123      * Returns the length.
124      * @return the length
125      */
126     @JsxGetter
127     @Override
128     public final int getLength() {
129         return super.getLength();
130     }
131 
132     /**
133      * {@inheritDoc}
134      */
135     @Override
136     public Object call(final Context cx, final Scriptable scope, final Scriptable thisObj, final Object[] args) {
137         if (supportsParentheses()) {
138             if (args.length == 0) {
139                 throw JavaScriptEngine.reportRuntimeError("Zero arguments; need an index or a key.");
140             }
141             final Object object = getIt(args[0]);
142             if (object == NOT_FOUND) {
143                 return null;
144             }
145             return object;
146         }
147 
148         throw JavaScriptEngine.typeError("HTMLCollection does nont support function like access");
149     }
150 
151     /**
152      * Is parentheses supported.
153      *
154      * @return true or false
155      */
156     protected boolean supportsParentheses() {
157         return false;
158     }
159 
160     /**
161      * {@inheritDoc}
162      */
163     @Override
164     protected Object getWithPreemptionByName(final String name, final List<DomNode> elements) {
165         final List<DomNode> matchingElements = new ArrayList<>();
166         final boolean searchName = isGetWithPreemptionSearchName();
167         for (final DomNode next : elements) {
168             if (next instanceof DomElement
169                     && (searchName || next instanceof HtmlInput || next instanceof HtmlForm)) {
170                 final String nodeName = ((DomElement) next).getAttributeDirect(DomElement.NAME_ATTRIBUTE);
171                 if (name.equals(nodeName)) {
172                     matchingElements.add(next);
173                 }
174             }
175         }
176 
177         if (matchingElements.isEmpty()) {
178             return NOT_FOUND;
179         }
180 
181         if (matchingElements.size() == 1) {
182             return getScriptableForElement(matchingElements.get(0));
183         }
184 
185         // many elements => build a sub collection
186         final DomNode domNode = getDomNodeOrNull();
187         final HTMLCollection collection = new HTMLCollection(domNode, matchingElements);
188         collection.setAvoidObjectDetection(true);
189         return collection;
190     }
191 
192     /**
193      * Returns whether {@link #getWithPreemption(String)} should search by name or not.
194      * @return whether {@link #getWithPreemption(String)} should search by name or not
195      */
196     protected boolean isGetWithPreemptionSearchName() {
197         return true;
198     }
199 
200     /**
201      * Returns the item or items corresponding to the specified index or key.
202      * @param index the index or key corresponding to the element or elements to return
203      * @return the element or elements corresponding to the specified index or key
204      * @see <a href="http://msdn.microsoft.com/en-us/library/ms536460.aspx">MSDN doc</a>
205      */
206     @JsxFunction
207     public Object item(final Object index) {
208         int idx = 0;
209         final double doubleValue = JavaScriptEngine.toNumber(index);
210         if (!Double.isNaN(doubleValue)) {
211             idx = (int) doubleValue;
212         }
213 
214         final Object object = get(idx, this);
215         if (object == NOT_FOUND) {
216             return null;
217         }
218         return object;
219     }
220 
221     /**
222      * Retrieves the item or items corresponding to the specified name (checks ids, and if
223      * that does not work, then names).
224      * @param name the name or id the element or elements to return
225      * @return the element or elements corresponding to the specified name or id
226      * @see <a href="http://msdn.microsoft.com/en-us/library/ms536634.aspx">MSDN doc</a>
227      */
228     @JsxFunction
229     public Scriptable namedItem(final String name) {
230         final List<DomNode> elements = getElements();
231         final BrowserVersion browserVersion = getBrowserVersion();
232         if (browserVersion.hasFeature(HTMLCOLLECTION_NAMED_ITEM_ID_FIRST)) {
233             for (final Object next : elements) {
234                 if (next instanceof DomElement) {
235                     final DomElement elem = (DomElement) next;
236                     final String id = elem.getId();
237                     if (name.equals(id)) {
238                         return getScriptableForElement(elem);
239                     }
240                 }
241             }
242         }
243         for (final Object next : elements) {
244             if (next instanceof DomElement) {
245                 final DomElement elem = (DomElement) next;
246                 final String nodeName = elem.getAttributeDirect(DomElement.NAME_ATTRIBUTE);
247                 if (name.equals(nodeName)) {
248                     return getScriptableForElement(elem);
249                 }
250 
251                 final String id = elem.getId();
252                 if (name.equals(id)) {
253                     return getScriptableForElement(elem);
254                 }
255             }
256         }
257         return null;
258     }
259 }