Skip to main content

script/dom/html/form_controls/
htmlbuttonelement.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::cell::Cell;
6use std::default::Default;
7
8use dom_struct::dom_struct;
9use html5ever::{LocalName, Prefix, local_name};
10use js::context::{JSContext, NoGC};
11use js::rust::HandleObject;
12use script_bindings::codegen::GenericBindings::DocumentBinding::DocumentMethods;
13use script_bindings::codegen::GenericBindings::DocumentFragmentBinding::DocumentFragmentMethods;
14use script_bindings::codegen::GenericBindings::NodeBinding::NodeMethods;
15use style::selector_parser::PseudoElement;
16use stylo_dom::ElementState;
17
18use crate::dom::FlatTreeParent;
19use crate::dom::activation::Activatable;
20use crate::dom::bindings::codegen::Bindings::HTMLButtonElementBinding::HTMLButtonElementMethods;
21use crate::dom::bindings::codegen::Bindings::NodeBinding::GetRootNodeOptions;
22use crate::dom::bindings::inheritance::Castable;
23use crate::dom::bindings::root::{DomRoot, MutNullableDom};
24use crate::dom::bindings::str::DOMString;
25use crate::dom::commandevent::CommandEvent;
26use crate::dom::document::Document;
27use crate::dom::documentfragment::DocumentFragment;
28use crate::dom::element::attributes::storage::AttrRef;
29use crate::dom::element::{AttributeMutation, Element};
30use crate::dom::event::{Event, EventBubbles, EventCancelable};
31use crate::dom::eventtarget::EventTarget;
32use crate::dom::html::htmlelement::HTMLElement;
33use crate::dom::html::htmlformelement::{
34    FormControl, FormDatum, FormDatumValue, FormSubmitterElement, HTMLFormElement, ResetFrom,
35    SubmittedFrom,
36};
37use crate::dom::node::virtualmethods::{VirtualMethods, vtable_for};
38use crate::dom::node::{Node, NodeTraits};
39use crate::dom::nodelist::NodeList;
40use crate::dom::types::HTMLInputElement;
41use crate::dom::validation::{Validatable, is_barred_by_datalist_ancestor};
42use crate::dom::validitystate::{ValidationFlags, ValidityState};
43
44#[derive(Clone, Copy, JSTraceable, MallocSizeOf, PartialEq)]
45enum ButtonType {
46    Submit,
47    Reset,
48    Button,
49}
50
51#[dom_struct]
52pub(crate) struct HTMLButtonElement {
53    htmlelement: HTMLElement,
54    button_type: Cell<ButtonType>,
55    form_owner: MutNullableDom<HTMLFormElement>,
56    labels_node_list: MutNullableDom<NodeList>,
57    validity_state: MutNullableDom<ValidityState>,
58}
59
60impl HTMLButtonElement {
61    fn new_inherited(
62        local_name: LocalName,
63        prefix: Option<Prefix>,
64        document: &Document,
65    ) -> HTMLButtonElement {
66        HTMLButtonElement {
67            htmlelement: HTMLElement::new_inherited_with_state(
68                ElementState::ENABLED,
69                local_name,
70                prefix,
71                document,
72            ),
73            button_type: Cell::new(ButtonType::Submit),
74            form_owner: Default::default(),
75            labels_node_list: Default::default(),
76            validity_state: Default::default(),
77        }
78    }
79
80    pub(crate) fn new(
81        cx: &mut js::context::JSContext,
82        local_name: LocalName,
83        prefix: Option<Prefix>,
84        document: &Document,
85        proto: Option<HandleObject>,
86    ) -> DomRoot<HTMLButtonElement> {
87        Node::reflect_node_with_proto(
88            cx,
89            Box::new(HTMLButtonElement::new_inherited(
90                local_name, prefix, document,
91            )),
92            document,
93            proto,
94        )
95    }
96
97    #[inline]
98    pub(crate) fn is_submit_button(&self) -> bool {
99        self.button_type.get() == ButtonType::Submit
100    }
101}
102
103impl HTMLButtonElementMethods<crate::DomTypeHolder> for HTMLButtonElement {
104    /// <https://html.spec.whatwg.org/multipage/#dom-button-command>
105    fn Command(&self) -> DOMString {
106        // Step 1. Let command be this's command attribute.
107        match self.command_state() {
108            // Step 2. If command is in the Custom state, then return command's value.
109            CommandState::Custom => self
110                .upcast::<Element>()
111                .get_string_attribute(&local_name!("command")),
112            // Step 3. If command is in the Unknown state, then return the empty string.
113            CommandState::Unknown => DOMString::default(),
114            // Step 4. Return the keyword corresponding to the value of command.
115            CommandState::Close => DOMString::from_static("close"),
116            CommandState::ShowModal => DOMString::from_static("show-modal"),
117        }
118    }
119
120    // https://html.spec.whatwg.org/multipage/#dom-button-command
121    make_setter!(SetCommand, "command");
122
123    // https://html.spec.whatwg.org/multipage/#dom-fe-disabled
124    make_bool_getter!(Disabled, "disabled");
125
126    // https://html.spec.whatwg.org/multipage/#dom-fe-disabled
127    make_bool_setter!(SetDisabled, "disabled");
128
129    /// <https://html.spec.whatwg.org/multipage/#dom-fae-form>
130    fn GetForm(&self) -> Option<DomRoot<HTMLFormElement>> {
131        self.form_owner()
132    }
133
134    /// <https://html.spec.whatwg.org/multipage/#dom-button-type>
135    fn Type(&self) -> DOMString {
136        match self.button_type.get() {
137            ButtonType::Submit => DOMString::from_static("submit"),
138            ButtonType::Button => DOMString::from_static("button"),
139            ButtonType::Reset => DOMString::from_static("reset"),
140        }
141    }
142
143    // https://html.spec.whatwg.org/multipage/#dom-button-type
144    make_setter!(SetType, "type");
145
146    // https://html.spec.whatwg.org/multipage/#dom-fs-formaction
147    make_form_action_getter!(FormAction, "formaction");
148
149    // https://html.spec.whatwg.org/multipage/#dom-fs-formaction
150    make_setter!(SetFormAction, "formaction");
151
152    // https://html.spec.whatwg.org/multipage/#dom-fs-formenctype
153    make_enumerated_getter!(
154        FormEnctype,
155        "formenctype",
156        "application/x-www-form-urlencoded" | "multipart/form-data" | "text/plain",
157        invalid => "application/x-www-form-urlencoded"
158    );
159
160    // https://html.spec.whatwg.org/multipage/#dom-fs-formenctype
161    make_setter!(SetFormEnctype, "formenctype");
162
163    // https://html.spec.whatwg.org/multipage/#dom-fs-formmethod
164    make_enumerated_getter!(
165        FormMethod,
166        "formmethod",
167        "get" | "post" | "dialog",
168        invalid => "get"
169    );
170
171    // https://html.spec.whatwg.org/multipage/#dom-fs-formmethod
172    make_setter!(SetFormMethod, "formmethod");
173
174    // https://html.spec.whatwg.org/multipage/#dom-fs-formtarget
175    make_getter!(FormTarget, "formtarget");
176
177    // https://html.spec.whatwg.org/multipage/#dom-fs-formtarget
178    make_setter!(SetFormTarget, "formtarget");
179
180    // https://html.spec.whatwg.org/multipage/#attr-fs-formnovalidate
181    make_bool_getter!(FormNoValidate, "formnovalidate");
182
183    // https://html.spec.whatwg.org/multipage/#attr-fs-formnovalidate
184    make_bool_setter!(SetFormNoValidate, "formnovalidate");
185
186    // https://html.spec.whatwg.org/multipage/#dom-fe-name
187    make_getter!(Name, "name");
188
189    // https://html.spec.whatwg.org/multipage/#dom-fe-name
190    make_atomic_setter!(SetName, "name");
191
192    // https://html.spec.whatwg.org/multipage/#dom-button-value
193    make_getter!(Value, "value");
194
195    // https://html.spec.whatwg.org/multipage/#dom-button-value
196    make_setter!(SetValue, "value");
197
198    // https://html.spec.whatwg.org/multipage/#dom-lfe-labels
199    make_labels_getter!(Labels, labels_node_list);
200
201    /// <https://html.spec.whatwg.org/multipage/#dom-cva-willvalidate>
202    fn WillValidate(&self, no_gc: &NoGC) -> bool {
203        self.is_instance_validatable(no_gc)
204    }
205
206    /// <https://html.spec.whatwg.org/multipage/#dom-cva-validity>
207    fn Validity(&self, cx: &mut JSContext) -> DomRoot<ValidityState> {
208        self.validity_state(cx)
209    }
210
211    /// <https://html.spec.whatwg.org/multipage/#dom-cva-checkvalidity>
212    fn CheckValidity(&self, cx: &mut JSContext) -> bool {
213        self.check_validity(cx)
214    }
215
216    /// <https://html.spec.whatwg.org/multipage/#dom-cva-reportvalidity>
217    fn ReportValidity(&self, cx: &mut JSContext) -> bool {
218        self.report_validity(cx)
219    }
220
221    /// <https://html.spec.whatwg.org/multipage/#dom-cva-validationmessage>
222    fn ValidationMessage(&self, cx: &mut JSContext) -> DOMString {
223        self.validation_message(cx)
224    }
225
226    /// <https://html.spec.whatwg.org/multipage/#dom-cva-setcustomvalidity>
227    fn SetCustomValidity(&self, cx: &mut JSContext, error: DOMString) {
228        self.validity_state(cx).set_custom_error_message(cx, error);
229    }
230}
231
232impl HTMLButtonElement {
233    /// <https://html.spec.whatwg.org/multipage/#constructing-the-form-data-set>
234    /// Steps range from 3.1 to 3.7 (specific to HTMLButtonElement)
235    pub(crate) fn form_datum(&self, submitter: Option<FormSubmitterElement>) -> Option<FormDatum> {
236        // Step 3.1: disabled state check is in get_unclean_dataset
237
238        // Step 3.1: only run steps if this is the submitter
239        if let Some(FormSubmitterElement::Button(submitter)) = submitter {
240            if submitter != self {
241                return None;
242            }
243        } else {
244            return None;
245        }
246        // Step 3.2
247        let ty = self.Type();
248        // Step 3.4
249        let name = self.Name();
250
251        if name.is_empty() {
252            // Step 3.1: Must have a name
253            return None;
254        }
255
256        // Step 3.9
257        Some(FormDatum {
258            ty,
259            name,
260            value: FormDatumValue::String(self.Value()),
261        })
262    }
263
264    fn set_type(&self, cx: &mut JSContext, value: DOMString) {
265        let value = if value.eq_ignore_ascii_case("reset") {
266            ButtonType::Reset
267        } else if value.eq_ignore_ascii_case("button") {
268            ButtonType::Button
269        } else if value.eq_ignore_ascii_case("submit") {
270            ButtonType::Submit
271        } else {
272            let element = self.upcast::<Element>();
273            if element.has_attribute(&local_name!("command")) ||
274                element.has_attribute(&local_name!("commandfor"))
275            {
276                ButtonType::Button
277            } else {
278                ButtonType::Submit
279            }
280        };
281        self.button_type.set(value);
282        self.validity_state(cx)
283            .perform_validation_and_update(cx, ValidationFlags::all());
284    }
285
286    fn command_for_element(&self, cx: &mut JSContext) -> Option<DomRoot<Element>> {
287        let command_for_value = self
288            .upcast::<Element>()
289            .get_attribute_string_value(&local_name!("commandfor"))?
290            .into();
291
292        let root_node = self
293            .upcast::<Node>()
294            .GetRootNode(&GetRootNodeOptions::empty());
295
296        if let Some(document) = root_node.downcast::<Document>() {
297            return document.GetElementById(cx, command_for_value);
298        } else if let Some(document_fragment) = root_node.downcast::<DocumentFragment>() {
299            return document_fragment.GetElementById(cx, command_for_value);
300        }
301        unreachable!("Button element must be in a document or document fragment");
302    }
303
304    fn command_state(&self) -> CommandState {
305        let command = self
306            .upcast::<Element>()
307            .get_string_attribute(&local_name!("command"));
308        if command.starts_with_str("--") {
309            return CommandState::Custom;
310        }
311        if command.eq_ignore_ascii_case("close") {
312            return CommandState::Close;
313        }
314        if command.eq_ignore_ascii_case("show-modal") {
315            return CommandState::ShowModal;
316        }
317
318        CommandState::Unknown
319    }
320
321    /// <https://html.spec.whatwg.org/multipage/#determine-if-command-is-valid>
322    fn determine_if_command_is_valid_for_target(command: CommandState, target: &Element) -> bool {
323        // Step 1. If command is in the Unknown state, then return false.
324        if command == CommandState::Unknown {
325            return false;
326        }
327        // Step 2. If command is in the Custom state, then return true.
328        if command == CommandState::Custom {
329            return true;
330        }
331        // Step 3. If target is not an HTML element, then return false.
332        if !target.is_html_element() {
333            return false;
334        }
335        // TODO Step 4. If command is in any of the following states:
336        // - Toggle Popover
337        // - Show Popover
338        // - Hide Popover
339        // then return true.
340        // Step 5. If this standard does not define is valid command steps for target's local name, then return false.
341        // Step 6. Otherwise, return the result of running target's corresponding is valid command steps given command.
342        vtable_for(target.upcast::<Node>()).is_valid_command_steps(command)
343    }
344
345    /// <https://html.spec.whatwg.org/multipage/#the-button-element:concept-fe-optional-value>
346    pub(crate) fn optional_value(&self) -> Option<DOMString> {
347        // The element's optional value is the value of the element's value attribute,
348        // if there is one; otherwise null.
349        self.upcast::<Element>()
350            .get_attribute_string_value(&local_name!("value"))
351            .map(|value| value.into())
352    }
353}
354
355impl VirtualMethods for HTMLButtonElement {
356    fn super_type(&self) -> Option<&dyn VirtualMethods> {
357        Some(self.upcast::<HTMLElement>() as &dyn VirtualMethods)
358    }
359
360    fn attribute_mutated(
361        &self,
362        cx: &mut js::context::JSContext,
363        attr: AttrRef<'_>,
364        mutation: AttributeMutation,
365    ) {
366        self.super_type()
367            .unwrap()
368            .attribute_mutated(cx, attr, mutation);
369        match *attr.local_name() {
370            local_name!("disabled") => {
371                self.validity_state(cx)
372                    .perform_validation_and_update(cx, ValidationFlags::all());
373            },
374            local_name!("type") => self.set_type(cx, attr.to_dom_string()),
375            local_name!("command") => self.set_type(
376                cx,
377                self.upcast::<Element>()
378                    .get_string_attribute(&local_name!("type")),
379            ),
380            local_name!("commandfor") => self.set_type(
381                cx,
382                self.upcast::<Element>()
383                    .get_string_attribute(&local_name!("type")),
384            ),
385            local_name!("form") => {
386                self.form_attribute_mutated(cx, mutation);
387                self.validity_state(cx)
388                    .perform_validation_and_update(cx, ValidationFlags::empty());
389            },
390            _ => {},
391        }
392    }
393}
394
395impl FormControl for HTMLButtonElement {
396    fn form_owner(&self) -> Option<DomRoot<HTMLFormElement>> {
397        self.form_owner.get()
398    }
399
400    fn set_form_owner(&self, _cx: &mut JSContext, form: Option<&HTMLFormElement>) {
401        self.form_owner.set(form);
402    }
403
404    fn to_html_element(&self) -> &HTMLElement {
405        self.upcast::<HTMLElement>()
406    }
407}
408
409impl Validatable for HTMLButtonElement {
410    fn as_element(&self) -> &Element {
411        self.upcast()
412    }
413
414    fn validity_state(&self, cx: &mut JSContext) -> DomRoot<ValidityState> {
415        self.validity_state
416            .or_init(|| ValidityState::new(cx, &self.owner_window(), self.upcast()))
417    }
418
419    fn is_instance_validatable(&self, no_gc: &NoGC) -> bool {
420        // https://html.spec.whatwg.org/multipage/#the-button-element%3Abarred-from-constraint-validation
421        // https://html.spec.whatwg.org/multipage/#enabling-and-disabling-form-controls%3A-the-disabled-attribute%3Abarred-from-constraint-validation
422        // https://html.spec.whatwg.org/multipage/#the-datalist-element%3Abarred-from-constraint-validation
423        self.button_type.get() == ButtonType::Submit &&
424            !self.upcast::<Element>().disabled_state() &&
425            !is_barred_by_datalist_ancestor(no_gc, self.upcast())
426    }
427}
428
429impl Activatable for HTMLButtonElement {
430    fn as_element(&self) -> &Element {
431        self.upcast()
432    }
433
434    fn is_instance_activatable(&self) -> bool {
435        // https://html.spec.whatwg.org/multipage/#the-button-element
436        !self.upcast::<Element>().disabled_state()
437    }
438
439    /// <https://html.spec.whatwg.org/multipage/#the-button-element:activation-behaviour>
440    fn activation_behavior(
441        &self,
442        cx: &mut js::context::JSContext,
443        event: &Event,
444        target: &EventTarget,
445    ) {
446        // Step 2. If element's node document is not fully active, then return.
447        if !target
448            .downcast::<Node>()
449            .is_none_or(|node| node.owner_document().is_fully_active())
450        {
451            return;
452        }
453
454        let button_type = self.button_type.get();
455        // Step 3. If element has a form owner:
456        if let Some(owner) = self.form_owner() {
457            // Step 3.1 If element is a submit button, then submit element's form owner from element
458            // ..., and return.
459            if button_type == ButtonType::Submit {
460                owner.submit(
461                    cx,
462                    SubmittedFrom::NotFromForm,
463                    FormSubmitterElement::Button(self),
464                );
465                return;
466            }
467            // Step 3.2 If element's type attribute is in the Reset Button state, then reset
468            // element's form owner and return.
469            if button_type == ButtonType::Reset {
470                owner.reset(cx, ResetFrom::NotFromForm);
471                return;
472            }
473            // Step 3.3 If element's type attribute is in the Auto state, then return.
474            if button_type == ButtonType::Button &&
475                !self
476                    .upcast::<Element>()
477                    .get_string_attribute(&local_name!("type"))
478                    .eq_ignore_ascii_case("button")
479            {
480                return;
481            }
482        }
483        // Adhoc, this step is needed so that file inputs button activates the input.
484        if let Some(pseudo_element) = self.upcast::<Node>().implemented_pseudo_element() {
485            if pseudo_element == PseudoElement::FileSelectorButton {
486                let FlatTreeParent::Parent(parent) =
487                    self.upcast::<Node>().parent_in_flat_tree(cx.no_gc())
488                else {
489                    return;
490                };
491
492                parent
493                    .as_rooted()
494                    .downcast::<HTMLInputElement>()
495                    .expect("File select button should always be a child of an input element")
496                    .activation_behavior(cx, event, target);
497            }
498
499            return;
500        }
501
502        // Step 4. Let target be the result of running element's get the commandfor-associated
503        // element.
504        // Step 5. If target is not null:
505        if let Some(target) = self.command_for_element(cx) {
506            // Steps 5.1 Let command be element's command attribute.
507            let command = self.command_state();
508            // Step 5.2 If the result of determining if a command is valid for a target given command and target is false, then return.
509            if !Self::determine_if_command_is_valid_for_target(command, &target) {
510                return;
511            }
512            // Step 5.3 Let continue be the result of firing an event named command at target, using
513            // CommandEvent, with its command attribute initialized to command, its source attribute
514            // initialized to element, and its cancelable attribute initialized to true.
515            // TODO source attribute
516            // Step 5.4 If continue is false, then return.
517            let event = CommandEvent::new(
518                cx,
519                &self.owner_window(),
520                atom!("command"),
521                EventBubbles::DoesNotBubble,
522                EventCancelable::Cancelable,
523                Some(self.upcast()),
524                self.upcast::<Element>()
525                    .get_string_attribute(&local_name!("command")),
526            );
527            let event = event.upcast::<Event>();
528            if !event.fire(cx, target.upcast::<EventTarget>()) {
529                return;
530            }
531            // Step 5.5 If target is not connected, then return.
532            let target_node = target.upcast::<Node>();
533            if !target_node.is_connected() {
534                return;
535            }
536            // Step 5.6 If command is in the Custom state, then return.
537            if command == CommandState::Custom {
538                return;
539            }
540            // TODO Steps 5.7, 5.8, 5.9
541            // Step 5.10 Otherwise, if this standard defines command steps for target's local name,
542            // then run the corresponding command steps given target, element, and command.
543            let _ = vtable_for(target_node).command_steps(cx, DomRoot::from_ref(self), command);
544        }
545        // TODO Step 6 Otherwise, run the popover target attribute activation behavior given element
546        // and event's target.
547    }
548}
549
550#[derive(Copy, Clone, Eq, PartialEq, Debug)]
551pub(crate) enum CommandState {
552    Unknown,
553    Custom,
554    ShowModal,
555    Close,
556}