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 org.htmlunit.SgmlPage;
18  import org.htmlunit.WebAssert;
19  import org.htmlunit.corejs.javascript.Context;
20  import org.htmlunit.corejs.javascript.Scriptable;
21  import org.htmlunit.corejs.javascript.ScriptableObject;
22  import org.htmlunit.corejs.javascript.VarScope;
23  import org.htmlunit.html.ElementFactory;
24  import org.htmlunit.html.HtmlOption;
25  import org.htmlunit.html.HtmlSelect;
26  import org.htmlunit.javascript.HtmlUnitScriptable;
27  import org.htmlunit.javascript.JavaScriptEngine;
28  import org.htmlunit.javascript.configuration.JsxClass;
29  import org.htmlunit.javascript.configuration.JsxConstructor;
30  import org.htmlunit.javascript.configuration.JsxFunction;
31  import org.htmlunit.javascript.configuration.JsxGetter;
32  import org.htmlunit.javascript.configuration.JsxSetter;
33  import org.htmlunit.javascript.configuration.JsxSymbol;
34  import org.htmlunit.javascript.host.dom.DOMException;
35  
36  /**
37   * This is the array returned by the "options" property of Select.
38   *
39   * @author David K. Taylor
40   * @author Christian Sell
41   * @author Marc Guillemot
42   * @author Daniel Gredler
43   * @author Bruce Faulkner
44   * @author Ahmed Ashour
45   * @author Ronald Brill
46   *
47   * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLOptionsCollection">MDN Documentation</a>
48   */
49  @JsxClass
50  public class HTMLOptionsCollection extends HtmlUnitScriptable {
51  
52      private HtmlSelect htmlSelect_;
53  
54      /**
55       * Creates an instance.
56       */
57      public HTMLOptionsCollection() {
58          super();
59      }
60  
61      /**
62       * JavaScript constructor.
63       */
64      @JsxConstructor
65      public void jsConstructor() {
66          // nothing to do
67      }
68  
69      /**
70       * Creates an instance.
71       * @param parentScope parent scope
72       */
73      public HTMLOptionsCollection(final VarScope parentScope) {
74          super();
75          setParentScope(parentScope);
76          setPrototype(getPrototype(getClass()));
77      }
78  
79      /**
80       * Initializes this object.
81       * @param select the HtmlSelect that this object will retrieve elements from
82       */
83      public void initialize(final HtmlSelect select) {
84          WebAssert.notNull("select", select);
85          htmlSelect_ = select;
86      }
87  
88      /**
89       * Returns the object at the specified index.
90       *
91       * @param index the index
92       * @param start the object that get is being called on
93       * @return the object or NOT_FOUND
94       */
95      @Override
96      public Object get(final int index, final Scriptable start) {
97          if (htmlSelect_ == null || index < 0) {
98              return JavaScriptEngine.UNDEFINED;
99          }
100 
101         if (index >= htmlSelect_.getOptionSize()) {
102             return JavaScriptEngine.UNDEFINED;
103         }
104 
105         return getScriptableFor(htmlSelect_.getOption(index));
106     }
107 
108     /**
109      * {@inheritDoc}
110      */
111     @Override
112     public void put(final String name, final Scriptable start, final Object value) {
113         if (htmlSelect_ == null) {
114             // This object hasn't been initialized; it's probably being used as a prototype.
115             // Just pretend we didn't even see this invocation and let Rhino handle it.
116             super.put(name, start, value);
117             return;
118         }
119 
120         final HTMLSelectElement parent = htmlSelect_.getScriptableObject();
121 
122         if (!has(name, start) && ScriptableObject.hasProperty(parent, name)) {
123             ScriptableObject.putProperty(parent, name, value);
124         }
125         else {
126             super.put(name, start, value);
127         }
128     }
129 
130     /**
131      * Returns the object at the specified index.
132      *
133      * @param index the index
134      * @return the object or NOT_FOUND
135      */
136     @JsxFunction
137     public Object item(final int index) {
138         final Object item = get(index, this);
139         if (JavaScriptEngine.UNDEFINED == item) {
140             return null;
141         }
142         return item;
143     }
144 
145     /**
146      * Sets the index property.
147      * @param index the index
148      * @param start the scriptable object that was originally invoked for this property
149      * @param newValue the new value
150      */
151     @Override
152     public void put(final int index, final Scriptable start, final Object newValue) {
153         if (newValue == null) {
154             // Remove the indexed option.
155             htmlSelect_.removeOption(index);
156         }
157         else {
158             final HTMLOptionElement option = (HTMLOptionElement) newValue;
159             final HtmlOption htmlOption = (HtmlOption) option.getDomNodeOrNull();
160             if (index >= getLength()) {
161                 setLength(index);
162                 // Add a new option at the end.
163                 htmlSelect_.appendOption(htmlOption);
164             }
165             else {
166                 // Replace the indexed option.
167                 htmlSelect_.replaceOption(index, htmlOption);
168             }
169         }
170     }
171 
172     /**
173      * Returns the number of elements in this array.
174      *
175      * @return the number of elements in the array
176      */
177     @JsxGetter
178     public int getLength() {
179         return htmlSelect_.getOptionSize();
180     }
181 
182     /**
183      * Changes the number of options: removes options if the new length
184      * is less than the current one else add new empty options to reach the
185      * new length.
186      * @param newLength the new length property value
187      */
188     @JsxSetter
189     public void setLength(final int newLength) {
190         if (newLength < 0) {
191             return;
192         }
193 
194         final int currentLength = htmlSelect_.getOptionSize();
195         if (currentLength > newLength) {
196             htmlSelect_.setOptionSize(newLength);
197         }
198         else {
199             final SgmlPage page = htmlSelect_.getPage();
200             final ElementFactory factory = page.getWebClient().getPageCreator()
201                                             .getHtmlParser().getFactory(HtmlOption.TAG_NAME);
202             for (int i = currentLength; i < newLength; i++) {
203                 final HtmlOption option = (HtmlOption) factory.createElement(page, HtmlOption.TAG_NAME, null);
204                 htmlSelect_.appendOption(option);
205             }
206         }
207     }
208 
209     /**
210      * Adds a new item to the option collection.
211      *
212      * <p><b><i>Implementation Note:</i></b> The specification for the JavaScript add() method
213      * actually calls for the optional newIndex parameter to be an integer. However, the
214      * newIndex parameter is specified as an Object here rather than an int because of the
215      * way Rhino and HtmlUnit process optional parameters for the JavaScript method calls.
216      * If the newIndex parameter were specified as an int, then the Undefined value for an
217      * integer is specified as NaN (Not A Number, which is a Double value), but Rhino
218      * translates this value into 0 (perhaps correctly?) when converting NaN into an int.
219      * As a result, when the newIndex parameter is not specified, it is impossible to make
220      * a distinction between a caller of the form add(someObject) and add (someObject, 0).
221      * Since the behavior of these two call forms is different, the newIndex parameter is
222      * specified as an Object. If the newIndex parameter is not specified by the actual
223      * JavaScript code being run, then newIndex is of type org.htmlunit.corejs.javascript.Undefined.
224      * If the newIndex parameter is specified, then it should be of type java.lang.Number and
225      * can be converted into an integer value.</p>
226      *
227      * <p>This method will call the {@link #put(int, Scriptable, Object)} method for actually
228      * adding the element to the collection.</p>
229      *
230      * <p>Per the <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLOptionsCollection/add">MDN
231      * documentation</a> for the JavaScript add() method of the options collection,
232      * the index parameter is specified as follows:
233      * </p>
234      * <p>
235      * <i>Optional. Integer that specifies the index position in the collection where the element is
236      * placed. If no value is given, the method places the element at the end of the collection.</i>
237      * </p>
238      *
239      * @param newOptionObject the DomNode to insert in the collection
240      * @param beforeOptionObject An optional parameter which specifies the index position in the
241      *        collection where the element is placed. If no value is given, the method places
242      *        the element at the end of the collection.
243      *
244      * @see #put(int, Scriptable, Object)
245      */
246     @JsxFunction
247     public void add(final Object newOptionObject, final Object beforeOptionObject) {
248         final HtmlOption htmlOption = (HtmlOption) ((HTMLOptionElement) newOptionObject).getDomNodeOrNull();
249 
250         HtmlOption beforeOption = null;
251         // If newIndex was specified, then use it
252         if (beforeOptionObject instanceof Number) {
253             final int index = ((Integer) Context.jsToJava(beforeOptionObject, Integer.class)).intValue();
254             if (index < 0 || index >= getLength()) {
255                 // Add a new option at the end.
256                 htmlSelect_.appendOption(htmlOption);
257                 return;
258             }
259 
260             beforeOption = (HtmlOption) ((HTMLOptionElement) item(index)).getDomNodeOrDie();
261         }
262         else if (beforeOptionObject instanceof HTMLOptionElement element) {
263             beforeOption = (HtmlOption) element.getDomNodeOrDie();
264             if (beforeOption.getParentNode() != htmlSelect_) {
265                 throw JavaScriptEngine.asJavaScriptException(
266                         getWindow(),
267                         "Unknown option.",
268                         DOMException.NOT_FOUND_ERR);
269 
270             }
271         }
272 
273         if (null == beforeOption) {
274             htmlSelect_.appendOption(htmlOption);
275             return;
276         }
277 
278         beforeOption.insertBefore(htmlOption);
279     }
280 
281     /**
282      * Removes the option at the specified index.
283      * @param index the option index
284      */
285     @JsxFunction
286     public void remove(final int index) {
287         if (index < 0 || index >= getLength()) {
288             return;
289         }
290 
291         htmlSelect_.removeOption(index);
292     }
293 
294     /**
295      * Returns the value of the {@code selectedIndex} property.
296      * @return the {@code selectedIndex} property
297      */
298     @JsxGetter
299     public int getSelectedIndex() {
300         return htmlSelect_.getSelectedIndex();
301     }
302 
303     /**
304      * Sets the value of the {@code selectedIndex} property.
305      * @param index the new value
306      */
307     @JsxSetter
308     public void setSelectedIndex(final int index) {
309         htmlSelect_.setSelectedIndex(index);
310     }
311 
312     /**
313      * Returns the {@code Symbol.iterator} function that allows iterating over this collection.
314      * @return the Iterator symbol
315      */
316     @JsxSymbol
317     public Scriptable iterator() {
318         return JavaScriptEngine.newArrayIteratorTypeValues(getParentScope(), this);
319     }
320 }