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 }