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 }