View Javadoc
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 static org.htmlunit.BrowserVersionFeatures.JS_SELECT_REMOVE_IGNORE_IF_INDEX_OUTSIDE;
18  
19  import java.util.List;
20  
21  import org.htmlunit.corejs.javascript.Context;
22  import org.htmlunit.corejs.javascript.Function;
23  import org.htmlunit.corejs.javascript.Scriptable;
24  import org.htmlunit.corejs.javascript.VarScope;
25  import org.htmlunit.html.HtmlOption;
26  import org.htmlunit.html.HtmlSelect;
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.Node;
35  import org.htmlunit.javascript.host.dom.NodeList;
36  
37  /**
38   * The JavaScript object for {@link HtmlSelect}.
39   *
40   * @author Mike Bowler
41   * @author David K. Taylor
42   * @author Marc Guillemot
43   * @author Chris Erskine
44   * @author Ahmed Ashour
45   * @author Ronald Brill
46   * @author Carsten Steul
47   */
48  @JsxClass(domClass = HtmlSelect.class)
49  public class HTMLSelectElement extends HTMLElement {
50  
51      private HTMLOptionsCollection optionsArray_;
52  
53      /** "Live" labels collection; has to be a member to have equality (==) working. */
54      private NodeList labels_;
55  
56      /**
57       * JavaScript constructor.
58       */
59      @Override
60      @JsxConstructor
61      public void jsConstructor() {
62          super.jsConstructor();
63      }
64  
65      /**
66       * Initialize the object.
67       *
68       */
69      public void initialize() {
70          final HtmlSelect htmlSelect = getDomNodeOrDie();
71          htmlSelect.setScriptableObject(this);
72          if (optionsArray_ == null) {
73              optionsArray_ = new HTMLOptionsCollection(getParentScope());
74              optionsArray_.initialize(htmlSelect);
75          }
76      }
77  
78      /**
79       * {@inheritDoc}
80       */
81      @Override
82      public HtmlSelect getDomNodeOrDie() {
83          return (HtmlSelect) super.getDomNodeOrDie();
84      }
85  
86      /**
87       * Removes option at the specified index.
88       * @param context the context
89       * @param scope the scope
90       * @param thisObj this object
91       * @param args the arguments
92       * @param function the function
93       */
94      @JsxFunction
95      public static void remove(final Context context, final VarScope scope,
96              final Scriptable thisObj, final Object[] args, final Function function) {
97          if (!(thisObj instanceof HTMLSelectElement htmlSelect)) {
98              throw JavaScriptEngine.reportRuntimeError(
99                      "HTMLSelectElement.replace() failed - this is not a HTMLSelectElement");
100         }
101 
102         if (args.length == 0) {
103             htmlSelect.remove();
104             return;
105         }
106 
107         final int index = JavaScriptEngine.toInt32(args[0]);
108         if (index < 0
109                 && htmlSelect.getBrowserVersion().hasFeature(JS_SELECT_REMOVE_IGNORE_IF_INDEX_OUTSIDE)) {
110             return;
111         }
112 
113         final HTMLOptionsCollection options = htmlSelect.getOptions();
114         if (index >= options.getLength()
115                 && htmlSelect.getBrowserVersion().hasFeature(JS_SELECT_REMOVE_IGNORE_IF_INDEX_OUTSIDE)) {
116             return;
117         }
118 
119         htmlSelect.getOptions().remove(index);
120     }
121 
122     /**
123      * Adds a new item to the list (optionally) before the specified item.
124      * @param newOptionObject the DomNode to insert
125      * @param beforeOptionObject the DomNode to insert the previous element before (null if at end).
126      */
127     @JsxFunction
128     public void add(final HTMLOptionElement newOptionObject, final Object beforeOptionObject) {
129         getOptions().add(newOptionObject, beforeOptionObject);
130     }
131 
132     /**
133      * {@inheritDoc}
134      */
135     @Override
136     public Node appendChild(final Object childObject) {
137         final Node node = super.appendChild(childObject);
138         getDomNodeOrDie().ensureSelectedIndex();
139         return node;
140     }
141 
142     /**
143      * {@inheritDoc}
144      */
145     @Override
146     public Node insertBeforeImpl(final Object[] args) {
147         final Node node = super.insertBeforeImpl(args);
148         getDomNodeOrDie().ensureSelectedIndex();
149         return node;
150     }
151 
152     /**
153      * Gets the item at the specified index.
154      * @param index the position of the option to retrieve
155      * @return the option
156      */
157     @JsxFunction
158     public Object item(final int index) {
159         final Object option = getOptions().item(index);
160         if (JavaScriptEngine.isUndefined(option)) {
161             return null;
162         }
163         return option;
164     }
165 
166     /**
167      * Returns the type of this input.
168      * @return the type
169      */
170     @JsxGetter
171     public String getType() {
172         final String type;
173         if (getDomNodeOrDie().isMultipleSelectEnabled()) {
174             type = "select-multiple";
175         }
176         else {
177             type = "select-one";
178         }
179         return type;
180     }
181 
182     /**
183      * Returns the value of the {@code options} property.
184      * @return the {@code options} property
185      */
186     @JsxGetter
187     public HTMLOptionsCollection getOptions() {
188         if (optionsArray_ == null) {
189             initialize();
190         }
191         return optionsArray_;
192     }
193 
194     /**
195      * Returns the value of the {@code selectedIndex} property.
196      * @return the {@code selectedIndex} property
197      */
198     @JsxGetter
199     public int getSelectedIndex() {
200         return getDomNodeOrDie().getSelectedIndex();
201     }
202 
203     /**
204      * Sets the value of the {@code selectedIndex} property.
205      * @param index the new value
206      */
207     @JsxSetter
208     public void setSelectedIndex(final int index) {
209         getDomNodeOrDie().setSelectedIndex(index);
210     }
211 
212     /**
213      * Returns the actual value of the selected Option.
214      * @return the value
215      */
216     @Override
217     @JsxGetter
218     public String getValue() {
219         final List<HtmlOption> selectedOptions = getDomNodeOrDie().getSelectedOptions();
220         if (selectedOptions.isEmpty()) {
221             return "";
222         }
223         return ((HTMLOptionElement) selectedOptions.get(0).getScriptableObject()).getValue();
224     }
225 
226     /**
227      * Returns the value of the {@code length} property.
228      * @return the {@code length} property
229      */
230     @JsxGetter
231     public int getLength() {
232         return getOptions().getLength();
233     }
234 
235     /**
236      * Removes options by reducing the {@code length} property.
237      * @param newLength the new {@code length} property value
238      */
239     @JsxSetter
240     public void setLength(final int newLength) {
241         getOptions().setLength(newLength);
242     }
243 
244     /**
245      * Returns the specified indexed property.
246      * @param index the index of the property
247      * @param start the scriptable object that was originally queried for this property
248      * @return the property
249      */
250     @Override
251     public Object get(final int index, final Scriptable start) {
252         if (getDomNodeOrNull() == null) {
253             return NOT_FOUND; // typically for the prototype
254         }
255         return getOptions().get(index, start);
256     }
257 
258     /**
259      * Sets the index property.
260      * @param index the index
261      * @param start the scriptable object that was originally invoked for this property
262      * @param newValue the new value
263      */
264     @Override
265     public void put(final int index, final Scriptable start, final Object newValue) {
266         getOptions().put(index, start, newValue);
267     }
268 
269     /**
270      * Selects the option with the specified value.
271      * @param newValue the value of the option to select
272      */
273     @Override
274     @JsxSetter
275     public void setValue(final Object newValue) {
276         final String val = JavaScriptEngine.toString(newValue);
277         getDomNodeOrDie().setSelectedAttribute(val, true, false);
278     }
279 
280     /**
281      * Returns the {@code size} attribute.
282      * @return the {@code size} attribute
283      */
284     @JsxGetter
285     public int getSize() {
286         return getDomNodeOrDie().getSize();
287     }
288 
289     /**
290      * Sets the {@code size} attribute.
291      * @param size the {@code size} attribute
292      */
293     @JsxSetter
294     public void setSize(final String size) {
295         getDomNodeOrDie().setAttribute("size", size);
296     }
297 
298     /**
299      * Returns {@code true} if the {@code multiple} attribute is set.
300      * @return {@code true} if the {@code multiple} attribute is set
301      */
302     @JsxGetter
303     public boolean isMultiple() {
304         return getDomNodeOrDie().hasAttribute("multiple");
305     }
306 
307     /**
308      * Sets or clears the {@code multiple} attribute.
309      * @param multiple {@code true} to set the {@code multiple} attribute, {@code false} to clear it
310      */
311     @JsxSetter
312     public void setMultiple(final boolean multiple) {
313         if (multiple) {
314             getDomNodeOrDie().setAttribute("multiple", "multiple");
315         }
316         else {
317             getDomNodeOrDie().removeAttribute("multiple");
318         }
319     }
320 
321     /**
322      * Returns the labels associated with the element.
323      * @return the labels associated with the element
324      */
325     @JsxGetter
326     public NodeList getLabels() {
327         if (labels_ == null) {
328             labels_ = new LabelsNodeList(getDomNodeOrDie());
329         }
330         return labels_;
331     }
332 
333     /**
334      * Returns the {@code required} property.
335      * @return the {@code required} property
336      */
337     @JsxGetter
338     public boolean isRequired() {
339         return getDomNodeOrDie().isRequired();
340     }
341 
342     /**
343      * Sets the {@code required} property.
344      * @param required the new value
345      */
346     @JsxSetter
347     public void setRequired(final boolean required) {
348         getDomNodeOrDie().setRequired(required);
349     }
350 
351     /**
352      * {@inheritDoc}
353      */
354     @JsxGetter
355     @Override
356     public String getName() {
357         return super.getName();
358     }
359 
360     /**
361      * {@inheritDoc}
362      */
363     @JsxSetter
364     @Override
365     public void setName(final String newName) {
366         super.setName(newName);
367     }
368 
369     /**
370      * {@inheritDoc} Overridden to modify browser configurations.
371      */
372     @Override
373     @JsxGetter
374     public boolean isDisabled() {
375         return super.isDisabled();
376     }
377 
378     /**
379      * {@inheritDoc} Overridden to modify browser configurations.
380      */
381     @Override
382     @JsxSetter
383     public void setDisabled(final boolean disabled) {
384         super.setDisabled(disabled);
385     }
386 
387     /**
388      * {@inheritDoc}
389      */
390     @JsxGetter
391     @Override
392     public HTMLFormElement getForm() {
393         return super.getForm();
394     }
395 
396     /**
397      * Checks whether the element has any constraints and whether it satisfies them.
398      * @return if the element is valid
399      */
400     @JsxFunction
401     public boolean checkValidity() {
402         return getDomNodeOrDie().isValid();
403     }
404 
405     /**
406      * @return a ValidityState with the validity states that this element is in.
407      */
408     @JsxGetter
409     public ValidityState getValidity() {
410         final ValidityState validityState = new ValidityState();
411         validityState.setPrototype(getPrototype(validityState.getClass()));
412         validityState.setParentScope(getParentScope());
413         validityState.setDomNode(getDomNodeOrDie());
414         return validityState;
415     }
416 
417     /**
418      * @return whether the element is a candidate for constraint validation
419      */
420     @JsxGetter
421     public boolean isWillValidate() {
422         return getDomNodeOrDie().willValidate();
423     }
424 
425     /**
426      * Sets the custom validity message for the element to the specified message.
427      * @param message the new message
428      */
429     @JsxFunction
430     public void setCustomValidity(final String message) {
431         getDomNodeOrDie().setCustomValidity(message);
432     }
433 
434     /**
435      * @return the Iterator symbol
436      */
437     @JsxSymbol
438     public Scriptable iterator() {
439         return getOptions().iterator();
440     }
441 }