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