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 Christian Sell
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 if (index < 0 || index >= getLength()) {
283 return;
284 }
285
286 htmlSelect_.removeOption(index);
287 }
288
289 /**
290 * Returns the value of the {@code selectedIndex} property.
291 * @return the {@code selectedIndex} property
292 */
293 @JsxGetter
294 public int getSelectedIndex() {
295 return htmlSelect_.getSelectedIndex();
296 }
297
298 /**
299 * Sets the value of the {@code selectedIndex} property.
300 * @param index the new value
301 */
302 @JsxSetter
303 public void setSelectedIndex(final int index) {
304 htmlSelect_.setSelectedIndex(index);
305 }
306
307 /**
308 * @return the Iterator symbol
309 */
310 @JsxSymbol
311 public Scriptable iterator() {
312 return JavaScriptEngine.newArrayIteratorTypeValues(getParentScope(), this);
313 }
314 }