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_FORM_DISPATCHEVENT_SUBMITS;
18  
19  import java.io.Serializable;
20  import java.net.MalformedURLException;
21  import java.util.ArrayList;
22  import java.util.List;
23  import java.util.function.Supplier;
24  
25  import org.htmlunit.FormEncodingType;
26  import org.htmlunit.WebAssert;
27  import org.htmlunit.corejs.javascript.Context;
28  import org.htmlunit.corejs.javascript.Function;
29  import org.htmlunit.corejs.javascript.Scriptable;
30  import org.htmlunit.corejs.javascript.ScriptableObject;
31  import org.htmlunit.corejs.javascript.VarScope;
32  import org.htmlunit.html.DomElement;
33  import org.htmlunit.html.DomNode;
34  import org.htmlunit.html.HtmlAttributeChangeEvent;
35  import org.htmlunit.html.HtmlElement;
36  import org.htmlunit.html.HtmlForm;
37  import org.htmlunit.html.HtmlImage;
38  import org.htmlunit.html.HtmlPage;
39  import org.htmlunit.html.SubmittableElement;
40  import org.htmlunit.javascript.JavaScriptEngine;
41  import org.htmlunit.javascript.configuration.JsxClass;
42  import org.htmlunit.javascript.configuration.JsxConstructor;
43  import org.htmlunit.javascript.configuration.JsxFunction;
44  import org.htmlunit.javascript.configuration.JsxGetter;
45  import org.htmlunit.javascript.configuration.JsxSetter;
46  import org.htmlunit.javascript.configuration.JsxSymbol;
47  import org.htmlunit.javascript.host.dom.AbstractList.EffectOnCache;
48  import org.htmlunit.javascript.host.dom.DOMTokenList;
49  import org.htmlunit.javascript.host.dom.RadioNodeList;
50  import org.htmlunit.javascript.host.event.Event;
51  import org.htmlunit.util.MimeType;
52  
53  /**
54   * A JavaScript object {@code HTMLFormElement}.
55   *
56   * @author Mike Bowler
57   * @author Daniel Gredler
58   * @author Kent Tong
59   * @author Chris Erskine
60   * @author Marc Guillemot
61   * @author Ahmed Ashour
62   * @author Sudhan Moghe
63   * @author Ronald Brill
64   * @author Frank Danek
65   * @author Lai Quang Duong
66   *
67   * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement">MDN Documentation</a>
68   */
69  @JsxClass(domClass = HtmlForm.class)
70  public class HTMLFormElement extends HTMLElement implements Function {
71  
72      /**
73       * JavaScript constructor.
74       */
75      @Override
76      @JsxConstructor
77      public void jsConstructor() {
78          super.jsConstructor();
79      }
80  
81      /**
82       * Returns the value of the property {@code name}.
83       * @return the value of this property
84       */
85      @JsxGetter
86      @Override
87      public String getName() {
88          return getHtmlForm().getNameAttribute();
89      }
90  
91      /**
92       * Sets the value of the property {@code name}.
93       * @param name the new value
94       */
95      @JsxSetter
96      @Override
97      public void setName(final String name) {
98          getHtmlForm().setNameAttribute(name);
99      }
100 
101     /**
102      * Returns the value of the property {@code elements}.
103      * @return the value of this property
104      */
105     @JsxGetter
106     public HTMLFormControlsCollection getElements() {
107         final HtmlForm htmlForm = getHtmlForm();
108 
109         final HTMLFormControlsCollection elements = new HTMLFormControlsCollection(htmlForm, false) {
110             @Override
111             protected Object getWithPreemption(final String name) {
112                 final List<HtmlElement> elementsForName = findElements(name);
113                 if (elementsForName.isEmpty()) {
114                     return NOT_FOUND;
115                 }
116                 if (elementsForName.size() == 1) {
117                     return getScriptableFor(elementsForName.get(0));
118                 }
119 
120                 final List<DomNode> nodes = new ArrayList<>(elementsForName);
121                 final RadioNodeList nodeList = new RadioNodeList(getHtmlForm(), nodes);
122                 nodeList.setElementsSupplier(
123                         (Supplier<List<DomNode>> & Serializable) () -> new ArrayList<>(findElements(name)));
124                 return nodeList;
125             }
126         };
127 
128         elements.setElementsSupplier(
129                 (Supplier<List<DomNode>> & Serializable)
130                 () -> {
131                     final DomNode domNode = getDomNodeOrNull();
132                     if (domNode == null) {
133                         return new ArrayList<>();
134                     }
135                     return new ArrayList<>(((HtmlForm) domNode).getElementsJS());
136                 });
137 
138         elements.setEffectOnCacheFunction(
139                 (java.util.function.Function<HtmlAttributeChangeEvent, EffectOnCache> & Serializable)
140                 event -> EffectOnCache.NONE);
141 
142         return elements;
143     }
144 
145     /**
146      * Returns the {@code Symbol.iterator} function that allows iterating over the form's elements.
147      * @return the Iterator symbol
148      */
149     @JsxSymbol
150     public Scriptable iterator() {
151         return getElements().iterator();
152     }
153 
154     /**
155      * Returns the value of the property {@code length}.
156      * Does not count input {@code type=image} elements
157      * (<a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/length">MDN doc</a>)
158      * @return the value of this property
159      */
160     @JsxGetter
161     public int getLength() {
162         return getElements().getLength();
163     }
164 
165     /**
166      * Returns the value of the property {@code action}.
167      * @return the value of this property
168      */
169     @JsxGetter
170     public String getAction() {
171         final String action = getHtmlForm().getActionAttribute();
172 
173         try {
174             return ((HtmlPage) getHtmlForm().getPage()).getFullyQualifiedUrl(action).toExternalForm();
175         }
176         catch (final MalformedURLException ignored) {
177             // nothing, return action attribute
178         }
179         return action;
180     }
181 
182     /**
183      * Sets the value of the property {@code action}.
184      * @param action the new value
185      */
186     @JsxSetter
187     public void setAction(final String action) {
188         WebAssert.notNull("action", action);
189         getHtmlForm().setActionAttribute(action);
190     }
191 
192     /**
193      * Returns the value of the property {@code method}.
194      * @return the value of this property
195      */
196     @JsxGetter
197     public String getMethod() {
198         return getHtmlForm().getMethodAttribute();
199     }
200 
201     /**
202      * Sets the value of the property {@code method}.
203      * @param method the new value
204      */
205     @JsxSetter
206     public void setMethod(final String method) {
207         WebAssert.notNull("method", method);
208         getHtmlForm().setMethodAttribute(method);
209     }
210 
211     /**
212      * Returns the value of the property {@code target}.
213      * @return the value of this property
214      */
215     @JsxGetter
216     public String getTarget() {
217         return getHtmlForm().getTargetAttribute();
218     }
219 
220     /**
221      * Sets the value of the property {@code target}.
222      * @param target the new value
223      */
224     @JsxSetter
225     public void setTarget(final String target) {
226         WebAssert.notNull("target", target);
227         getHtmlForm().setTargetAttribute(target);
228     }
229 
230     /**
231      * Returns the value of the property {@code rel}.
232      * @return the value of this property
233      */
234     @JsxGetter
235     public String getRel() {
236         return getHtmlForm().getRelAttribute();
237     }
238 
239     /**
240      * Sets the value of the property {@code rel}.
241      * @param rel the new value
242      */
243     @JsxSetter
244     public void setRel(final String rel) {
245         getHtmlForm().setAttribute("rel", rel);
246     }
247 
248     /**
249      * Returns the {@code relList} attribute.
250      * @return the {@code relList} attribute
251      */
252     @JsxGetter
253     public DOMTokenList getRelList() {
254         return new DOMTokenList(this, "rel");
255     }
256 
257     /**
258      * Sets the {@code relList} attribute.
259      * @param rel the {@code relList} attribute value
260      */
261     @JsxSetter
262     public void setRelList(final Object rel) {
263         if (JavaScriptEngine.isUndefined(rel)) {
264             setRel("undefined");
265             return;
266         }
267         setRel(JavaScriptEngine.toString(rel));
268     }
269 
270     /**
271      * Returns the value of the property {@code enctype}.
272      * @return the value of this property
273      */
274     @JsxGetter
275     public String getEnctype() {
276         final String encoding = getHtmlForm().getEnctypeAttribute();
277         if (!FormEncodingType.URL_ENCODED.getName().equals(encoding)
278                 && !FormEncodingType.MULTIPART.getName().equals(encoding)
279                 && !MimeType.TEXT_PLAIN.equals(encoding)) {
280             return FormEncodingType.URL_ENCODED.getName();
281         }
282         return encoding;
283     }
284 
285     /**
286      * Sets the value of the property {@code enctype}.
287      * @param enctype the new value
288      */
289     @JsxSetter
290     public void setEnctype(final String enctype) {
291         WebAssert.notNull("encoding", enctype);
292         getHtmlForm().setEnctypeAttribute(enctype);
293     }
294 
295     /**
296      * Returns the value of the property {@code encoding}.
297      * @return the value of this property
298      */
299     @JsxGetter
300     public String getEncoding() {
301         return getEnctype();
302     }
303 
304     /**
305      * Sets the value of the property {@code encoding}.
306      * @param encoding the new value
307      */
308     @JsxSetter
309     public void setEncoding(final String encoding) {
310         setEnctype(encoding);
311     }
312 
313     /**
314      * Returns the {@link HtmlForm} associated with this element.
315      * @return the associated HtmlForm
316      */
317     public HtmlForm getHtmlForm() {
318         return (HtmlForm) getDomNodeOrDie();
319     }
320 
321     /**
322      * Submits the form (at the end of the current script execution).
323      */
324     @JsxFunction
325     public void submit() {
326         getHtmlForm().submit(null);
327     }
328 
329     /**
330      * Submits the form using the specified submit button.
331      * @param submitter the submit button whose attributes describe the method
332      *        by which the form is to be submitted. This may be either
333      *        a &lt;input&gt; or &lt;button&gt; element whose type attribute is submit.
334      *        If you omit the submitter parameter, the form element itself is used as the submitter.
335      */
336     @JsxFunction
337     public void requestSubmit(final Object submitter) {
338         if (JavaScriptEngine.isUndefined(submitter)) {
339             submit();
340             return;
341         }
342 
343         SubmittableElement submittable = null;
344         if (submitter instanceof HTMLElement subHtmlElement) {
345             if (subHtmlElement instanceof HTMLButtonElement element1) {
346                 if ("submit".equals(element1.getType())) {
347                     submittable = (SubmittableElement) subHtmlElement.getDomNodeOrDie();
348                 }
349             }
350             else if (subHtmlElement instanceof HTMLInputElement element) {
351                 if ("submit".equals(element.getType())) {
352                     submittable = (SubmittableElement) subHtmlElement.getDomNodeOrDie();
353                 }
354             }
355 
356             if (submittable != null && subHtmlElement.getForm() != this) {
357                 throw JavaScriptEngine.typeError(
358                         "Failed to execute 'requestSubmit' on 'HTMLFormElement': "
359                         + "The specified element is not owned by this form element.");
360             }
361         }
362 
363         if (submittable == null) {
364             throw JavaScriptEngine.typeError(
365                     "Failed to execute 'requestSubmit' on 'HTMLFormElement': "
366                     + "The specified element is not a submit button.");
367         }
368 
369         this.getHtmlForm().submit(submittable);
370     }
371 
372     /**
373      * Resets this form.
374      */
375     @JsxFunction
376     public void reset() {
377         getHtmlForm().reset();
378     }
379 
380     /**
381      * Overridden to allow the retrieval of certain form elements by ID or name.
382      * @see <a href="https://html.spec.whatwg.org/multipage/forms.html#dom-form-nameditem">
383      *     HTML spec - form named item</a>
384      *
385      * @param name {@inheritDoc}
386      * @return {@inheritDoc}
387      */
388     @Override
389     protected Object getWithPreemption(final String name) {
390         if (getDomNodeOrNull() == null) {
391             return NOT_FOUND;
392         }
393         final List<HtmlElement> elements = findElements(name);
394 
395         if (elements.isEmpty()) {
396             final HtmlElement element = getHtmlForm().getNamedElement(name);
397             return element != null ? getScriptableFor(element) : NOT_FOUND;
398         }
399         if (elements.size() == 1) {
400             final HtmlElement element = elements.get(0);
401             getHtmlForm().registerPastName(name, element);
402             return getScriptableFor(element);
403         }
404         final List<DomNode> nodes = new ArrayList<>(elements);
405 
406         final RadioNodeList nodeList = new RadioNodeList(getHtmlForm(), nodes);
407         nodeList.setElementsSupplier(
408                 (Supplier<List<DomNode>> & Serializable)
409                 () -> new ArrayList<>(findElements(name)));
410         return nodeList;
411     }
412 
413     /**
414      * Overridden to allow the retrieval of certain form elements by ID or name.
415      *
416      * @param name {@inheritDoc}
417      * @param start {@inheritDoc}
418      * @return {@inheritDoc}
419      */
420     @Override
421     public boolean has(final String name, final Scriptable start) {
422         if (super.has(name, start)) {
423             return true;
424         }
425 
426         return findFirstElement(name) != null;
427     }
428 
429     /**
430      * Overridden to allow the retrieval of certain form elements by ID or name.
431      *
432      * @param cx {@inheritDoc}
433      * @param id {@inheritDoc}
434      * @return {@inheritDoc}
435      */
436     @Override
437     protected DescriptorInfo getOwnPropertyDescriptor(final Context cx, final Object id) {
438         final DescriptorInfo descInfo = super.getOwnPropertyDescriptor(cx, id);
439         if (descInfo != null) {
440             return descInfo;
441         }
442 
443         if (id instanceof CharSequence) {
444             final HtmlElement element = findFirstElement(id.toString());
445             if (element != null) {
446                 return ScriptableObject.buildDataDescriptor(element.getScriptableObject(),
447                                             ScriptableObject.READONLY | ScriptableObject.DONTENUM);
448             }
449         }
450 
451         return null;
452     }
453 
454     List<HtmlElement> findElements(final String name) {
455         final List<HtmlElement> elements = new ArrayList<>();
456         final HtmlForm form = (HtmlForm) getDomNodeOrNull();
457         if (form == null) {
458             return elements;
459         }
460 
461         for (final HtmlElement element : form.getElementsJS()) {
462             if (name.equals(element.getId())
463                     || name.equals(element.getAttributeDirect(DomElement.NAME_ATTRIBUTE))) {
464                 elements.add(element);
465             }
466         }
467 
468         // If no form fields are found, browsers are able to find img elements by ID or name.
469         if (elements.isEmpty()) {
470             for (final DomNode node : form.getHtmlElementDescendants()) {
471                 if (node instanceof HtmlImage img) {
472                     if (name.equals(img.getId()) || name.equals(img.getNameAttribute())) {
473                         elements.add(img);
474                     }
475                 }
476             }
477         }
478 
479         return elements;
480     }
481 
482     private HtmlElement findFirstElement(final String name) {
483         final HtmlForm form = (HtmlForm) getDomNodeOrNull();
484         if (form == null) {
485             return null;
486         }
487 
488         for (final HtmlElement node : form.getElementsJS()) {
489             if (name.equals(node.getId())
490                     || name.equals(node.getAttributeDirect(DomElement.NAME_ATTRIBUTE))) {
491                 return node;
492             }
493         }
494 
495         // If no form fields are found, browsers are able to find img elements by ID or name.
496         for (final DomNode node : form.getHtmlElementDescendants()) {
497             if (node instanceof HtmlImage img) {
498                 if (name.equals(img.getId()) || name.equals(img.getNameAttribute())) {
499                     return img;
500                 }
501             }
502         }
503 
504         return null;
505     }
506 
507     /**
508      * Returns the specified indexed property.
509      * @param index the index of the property
510      * @param start the scriptable object that was originally queried for this property
511      * @return the property
512      */
513     @Override
514     public Object get(final int index, final Scriptable start) {
515         if (getDomNodeOrNull() == null) {
516             return NOT_FOUND; // typically for the prototype
517         }
518         return getElements().get(index, ((HTMLFormElement) start).getElements());
519     }
520 
521     /**
522      * {@inheritDoc}
523      */
524     @Override
525     public Object call(final Context cx, final VarScope scope, final Scriptable thisObj, final Object[] args) {
526         throw JavaScriptEngine.typeError("Not a function.");
527     }
528 
529     /**
530      * {@inheritDoc}
531      */
532     @Override
533     public Scriptable construct(final Context cx, final VarScope scope, final Object[] args) {
534         throw JavaScriptEngine.typeError("Not a function.");
535     }
536 
537     @Override
538     public boolean dispatchEvent(final Event event) {
539         final boolean result = super.dispatchEvent(event);
540 
541         if (Event.TYPE_SUBMIT.equals(event.getType())
542                 && getBrowserVersion().hasFeature(JS_FORM_DISPATCHEVENT_SUBMITS)) {
543             submit();
544         }
545         return result;
546     }
547 
548     /**
549      * Checks whether the element has any constraints and whether it satisfies them.
550      * @return {@code true} if the element is valid
551      */
552     @JsxFunction
553     public boolean checkValidity() {
554         return getDomNodeOrDie().isValid();
555     }
556 
557     /**
558      * Returns the value of the property {@code novalidate}.
559      * @return the value of this property
560      */
561     @JsxGetter
562     public boolean isNoValidate() {
563         return getHtmlForm().isNoValidate();
564     }
565 
566     /**
567      * Sets the value of the property {@code novalidate}.
568      * @param value the new value
569      */
570     @JsxSetter
571     public void setNoValidate(final boolean value) {
572         getHtmlForm().setNoValidate(value);
573     }
574 }