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