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