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