gecko-dev/servo/components/script/dom/htmlinputelement.rs
Mikko Vanhatalo 22b4c1f779 servo: Merge #5385 - Implemented HTMLInputElement placeholder attribute (from minsumm:placeholder); r=jdm
I'm unsure whether I should wrap 'placeholder' in Cell, or DomRefCell, or leave as it is now.
Also, the spec says that the placeholder should be presented with line breaks stripped off,
should it be done in this stage?

Source-Repo: https://github.com/servo/servo
Source-Revision: 4ffeb81aa73fa87120eabb569fd14d7193813bdf
2015-04-04 08:24:48 -06:00

826 lines
34 KiB
Rust

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
use dom::activation::Activatable;
use dom::attr::{Attr, AttrValue};
use dom::attr::AttrHelpers;
use dom::bindings::cell::DOMRefCell;
use dom::bindings::codegen::Bindings::AttrBinding::AttrMethods;
use dom::bindings::codegen::Bindings::EventBinding::EventMethods;
use dom::bindings::codegen::Bindings::HTMLInputElementBinding;
use dom::bindings::codegen::Bindings::HTMLInputElementBinding::HTMLInputElementMethods;
use dom::bindings::codegen::InheritTypes::{ElementCast, HTMLElementCast, HTMLInputElementCast, NodeCast};
use dom::bindings::codegen::InheritTypes::{HTMLInputElementDerived, HTMLFieldSetElementDerived, EventTargetCast};
use dom::bindings::codegen::InheritTypes::KeyboardEventCast;
use dom::bindings::global::GlobalRef;
use dom::bindings::js::{Comparable, JSRef, LayoutJS, Root, Temporary, OptionalRootable};
use dom::bindings::js::{ResultRootable, RootedReference, MutNullableJS};
use dom::document::{Document, DocumentHelpers};
use dom::element::{AttributeHandlers, Element};
use dom::element::{RawLayoutElementHelpers, ActivationElementHelpers};
use dom::event::{Event, EventBubbles, EventCancelable, EventHelpers};
use dom::eventtarget::{EventTarget, EventTargetTypeId};
use dom::element::ElementTypeId;
use dom::htmlelement::{HTMLElement, HTMLElementTypeId};
use dom::keyboardevent::KeyboardEvent;
use dom::htmlformelement::{FormSubmitter, FormControl, HTMLFormElement, HTMLFormElementHelpers};
use dom::htmlformelement::{SubmittedFrom, ResetFrom};
use dom::node::{DisabledStateHelpers, Node, NodeHelpers, NodeDamage, NodeTypeId};
use dom::node::{document_from_node, window_from_node};
use dom::virtualmethods::VirtualMethods;
use textinput::TextInput;
use textinput::KeyReaction::{TriggerDefaultAction, DispatchInput, Nothing};
use textinput::Lines::Single;
use util::str::DOMString;
use string_cache::Atom;
use std::ascii::OwnedAsciiExt;
use std::borrow::ToOwned;
use std::cell::Cell;
use std::default::Default;
const DEFAULT_SUBMIT_VALUE: &'static str = "Submit";
const DEFAULT_RESET_VALUE: &'static str = "Reset";
#[jstraceable]
#[derive(PartialEq, Copy)]
#[allow(dead_code)]
enum InputType {
InputSubmit,
InputReset,
InputButton,
InputText,
InputFile,
InputImage,
InputCheckbox,
InputRadio,
InputPassword
}
#[dom_struct]
pub struct HTMLInputElement {
htmlelement: HTMLElement,
input_type: Cell<InputType>,
checked: Cell<bool>,
checked_changed: Cell<bool>,
placeholder: DOMRefCell<DOMString>,
indeterminate: Cell<bool>,
value_changed: Cell<bool>,
size: Cell<u32>,
textinput: DOMRefCell<TextInput>,
activation_state: DOMRefCell<InputActivationState>,
}
#[jstraceable]
#[must_root]
struct InputActivationState {
indeterminate: bool,
checked: bool,
checked_changed: bool,
checked_radio: MutNullableJS<HTMLInputElement>,
// In case mutability changed
was_mutable: bool,
// In case the type changed
old_type: InputType,
}
impl InputActivationState {
fn new() -> InputActivationState {
InputActivationState {
indeterminate: false,
checked: false,
checked_changed: false,
checked_radio: Default::default(),
was_mutable: false,
old_type: InputType::InputText
}
}
}
impl HTMLInputElementDerived for EventTarget {
fn is_htmlinputelement(&self) -> bool {
*self.type_id() == EventTargetTypeId::Node(NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLInputElement)))
}
}
static DEFAULT_INPUT_SIZE: u32 = 20;
impl HTMLInputElement {
fn new_inherited(localName: DOMString, prefix: Option<DOMString>, document: JSRef<Document>) -> HTMLInputElement {
HTMLInputElement {
htmlelement: HTMLElement::new_inherited(HTMLElementTypeId::HTMLInputElement, localName, prefix, document),
input_type: Cell::new(InputType::InputText),
checked: Cell::new(false),
placeholder: DOMRefCell::new("".to_owned()),
indeterminate: Cell::new(false),
checked_changed: Cell::new(false),
value_changed: Cell::new(false),
size: Cell::new(DEFAULT_INPUT_SIZE),
textinput: DOMRefCell::new(TextInput::new(Single, "".to_owned())),
activation_state: DOMRefCell::new(InputActivationState::new())
}
}
#[allow(unrooted_must_root)]
pub fn new(localName: DOMString, prefix: Option<DOMString>, document: JSRef<Document>) -> Temporary<HTMLInputElement> {
let element = HTMLInputElement::new_inherited(localName, prefix, document);
Node::reflect_node(box element, document, HTMLInputElementBinding::Wrap)
}
}
pub trait LayoutHTMLInputElementHelpers {
#[allow(unsafe_code)]
unsafe fn get_value_for_layout(self) -> String;
#[allow(unsafe_code)]
unsafe fn get_size_for_layout(self) -> u32;
}
pub trait RawLayoutHTMLInputElementHelpers {
#[allow(unsafe_code)]
unsafe fn get_checked_state_for_layout(&self) -> bool;
#[allow(unsafe_code)]
unsafe fn get_indeterminate_state_for_layout(&self) -> bool;
#[allow(unsafe_code)]
unsafe fn get_size_for_layout(&self) -> u32;
}
impl LayoutHTMLInputElementHelpers for LayoutJS<HTMLInputElement> {
#[allow(unrooted_must_root)]
#[allow(unsafe_code)]
unsafe fn get_value_for_layout(self) -> String {
#[allow(unsafe_code)]
unsafe fn get_raw_textinput_value(input: LayoutJS<HTMLInputElement>) -> String {
let textinput = (*input.unsafe_get()).textinput.borrow_for_layout().get_content();
if !textinput.is_empty() {
textinput
} else {
(*input.unsafe_get()).placeholder.borrow_for_layout().to_owned()
}
}
#[allow(unsafe_code)]
unsafe fn get_raw_attr_value(input: LayoutJS<HTMLInputElement>) -> Option<String> {
let elem: LayoutJS<Element> = input.transmute_copy();
(*elem.unsafe_get()).get_attr_val_for_layout(&ns!(""), &atom!("value"))
.map(|s| s.to_owned())
}
match (*self.unsafe_get()).input_type.get() {
InputType::InputCheckbox | InputType::InputRadio => "".to_owned(),
InputType::InputFile | InputType::InputImage => "".to_owned(),
InputType::InputButton => get_raw_attr_value(self).unwrap_or_else(|| "".to_owned()),
InputType::InputSubmit => get_raw_attr_value(self).unwrap_or_else(|| DEFAULT_SUBMIT_VALUE.to_owned()),
InputType::InputReset => get_raw_attr_value(self).unwrap_or_else(|| DEFAULT_RESET_VALUE.to_owned()),
InputType::InputPassword => {
let raw = get_raw_textinput_value(self);
raw.chars().map(|_| '●').collect()
}
_ => get_raw_textinput_value(self),
}
}
#[allow(unrooted_must_root)]
#[allow(unsafe_code)]
unsafe fn get_size_for_layout(self) -> u32 {
(*self.unsafe_get()).get_size_for_layout()
}
}
impl RawLayoutHTMLInputElementHelpers for HTMLInputElement {
#[allow(unrooted_must_root)]
#[allow(unsafe_code)]
unsafe fn get_checked_state_for_layout(&self) -> bool {
self.checked.get()
}
#[allow(unrooted_must_root)]
#[allow(unsafe_code)]
unsafe fn get_indeterminate_state_for_layout(&self) -> bool {
self.indeterminate.get()
}
#[allow(unrooted_must_root)]
#[allow(unsafe_code)]
unsafe fn get_size_for_layout(&self) -> u32 {
self.size.get()
}
}
impl<'a> HTMLInputElementMethods for JSRef<'a, HTMLInputElement> {
// http://www.whatwg.org/html/#dom-fe-disabled
make_bool_getter!(Disabled);
// http://www.whatwg.org/html/#dom-fe-disabled
make_bool_setter!(SetDisabled, "disabled");
// https://html.spec.whatwg.org/multipage/forms.html#dom-input-defaultchecked
make_bool_getter!(DefaultChecked, "checked");
// https://html.spec.whatwg.org/multipage/forms.html#dom-input-defaultchecked
make_bool_setter!(SetDefaultChecked, "checked");
// https://html.spec.whatwg.org/multipage/forms.html#dom-input-checked
fn Checked(self) -> bool {
self.checked.get()
}
// https://html.spec.whatwg.org/multipage/forms.html#dom-input-checked
fn SetChecked(self, checked: bool) {
self.update_checked_state(checked, true);
}
// https://html.spec.whatwg.org/multipage/forms.html#dom-input-readonly
make_bool_getter!(ReadOnly);
// https://html.spec.whatwg.org/multipage/forms.html#dom-input-readonly
make_bool_setter!(SetReadOnly, "readonly");
// https://html.spec.whatwg.org/multipage/forms.html#dom-input-size
make_uint_getter!(Size);
// https://html.spec.whatwg.org/multipage/forms.html#dom-input-size
make_uint_setter!(SetSize, "size");
// https://html.spec.whatwg.org/multipage/forms.html#dom-input-type
make_enumerated_getter!(Type, "text", ("hidden") | ("search") | ("tel") |
("url") | ("email") | ("password") |
("datetime") | ("date") | ("month") |
("week") | ("time") | ("datetime-local") |
("number") | ("range") | ("color") |
("checkbox") | ("radio") | ("file") |
("submit") | ("image") | ("reset") | ("button"));
// https://html.spec.whatwg.org/multipage/forms.html#dom-input-type
make_setter!(SetType, "type");
// https://html.spec.whatwg.org/multipage/forms.html#dom-input-value
fn Value(self) -> DOMString {
// FIXME(https://github.com/rust-lang/rust/issues/23338)
let textinput = self.textinput.borrow();
textinput.get_content()
}
// https://html.spec.whatwg.org/multipage/forms.html#dom-input-value
fn SetValue(self, value: DOMString) {
self.textinput.borrow_mut().set_content(value);
self.value_changed.set(true);
self.force_relayout();
}
// https://html.spec.whatwg.org/multipage/forms.html#dom-input-defaultvalue
make_getter!(DefaultValue, "value");
// https://html.spec.whatwg.org/multipage/forms.html#dom-input-defaultvalue
make_setter!(SetDefaultValue, "value");
// https://html.spec.whatwg.org/multipage/forms.html#attr-fe-name
make_getter!(Name);
// https://html.spec.whatwg.org/multipage/forms.html#attr-fe-name
make_setter!(SetName, "name");
// https://html.spec.whatwg.org/multipage/forms.html#attr-input-placeholder
make_getter!(Placeholder);
// https://html.spec.whatwg.org/multipage/forms.html#attr-input-placeholder
make_setter!(SetPlaceholder, "placeholder");
// https://html.spec.whatwg.org/multipage/forms.html#dom-input-formaction
make_url_or_base_getter!(FormAction);
// https://html.spec.whatwg.org/multipage/forms.html#dom-input-formaction
make_setter!(SetFormAction, "formaction");
// https://html.spec.whatwg.org/multipage/forms.html#dom-input-formenctype
make_enumerated_getter!(FormEnctype, "application/x-www-form-urlencoded", ("text/plain") | ("multipart/form-data"));
// https://html.spec.whatwg.org/multipage/forms.html#dom-input-formenctype
make_setter!(SetFormEnctype, "formenctype");
// https://html.spec.whatwg.org/multipage/forms.html#dom-input-formmethod
make_enumerated_getter!(FormMethod, "get", ("post") | ("dialog"));
// https://html.spec.whatwg.org/multipage/forms.html#dom-input-formmethod
make_setter!(SetFormMethod, "formmethod");
// https://html.spec.whatwg.org/multipage/forms.html#dom-input-formtarget
make_getter!(FormTarget);
// https://html.spec.whatwg.org/multipage/forms.html#dom-input-formtarget
make_setter!(SetFormTarget, "formtarget");
// https://html.spec.whatwg.org/multipage/forms.html#dom-input-indeterminate
fn Indeterminate(self) -> bool {
self.indeterminate.get()
}
// https://html.spec.whatwg.org/multipage/forms.html#dom-input-indeterminate
fn SetIndeterminate(self, val: bool) {
self.indeterminate.set(val)
}
}
pub trait HTMLInputElementHelpers {
fn force_relayout(self);
fn radio_group_updated(self, group: Option<&str>);
fn get_radio_group_name(self) -> Option<String>;
fn update_checked_state(self, checked: bool, dirty: bool);
fn get_size(&self) -> u32;
fn get_indeterminate_state(self) -> bool;
fn mutable(self) -> bool;
fn reset(self);
}
#[allow(unsafe_code)]
fn broadcast_radio_checked(broadcaster: JSRef<HTMLInputElement>, group: Option<&str>) {
//TODO: if not in document, use root ancestor instead of document
let owner = broadcaster.form_owner().root();
let doc = document_from_node(broadcaster).root();
let doc_node: JSRef<Node> = NodeCast::from_ref(doc.r());
// This function is a workaround for lifetime constraint difficulties.
fn do_broadcast<'a>(doc_node: JSRef<'a, Node>, broadcaster: JSRef<'a, HTMLInputElement>,
owner: Option<JSRef<'a, HTMLFormElement>>, group: Option<&str>) {
// There is no DOM tree manipulation here, so this is safe
let iter = unsafe {
doc_node.query_selector_iter("input[type=radio]".to_owned()).unwrap()
.filter_map(|t| HTMLInputElementCast::to_ref(t))
.filter(|&r| in_same_group(r, owner, group) && broadcaster != r)
};
for r in iter {
if r.Checked() {
r.SetChecked(false);
}
}
}
do_broadcast(doc_node, broadcaster, owner.r(), group)
}
fn in_same_group<'a,'b>(other: JSRef<'a, HTMLInputElement>,
owner: Option<JSRef<'b, HTMLFormElement>>,
group: Option<&str>) -> bool {
let other_owner = other.form_owner().root();
let other_owner = other_owner.r();
other.input_type.get() == InputType::InputRadio &&
// TODO Both a and b are in the same home subtree.
other_owner.equals(owner) &&
// TODO should be a unicode compatibility caseless match
match (other.get_radio_group_name(), group) {
(Some(ref s1), Some(s2)) => s1.as_slice() == s2,
(None, None) => true,
_ => false
}
}
impl<'a> HTMLInputElementHelpers for JSRef<'a, HTMLInputElement> {
fn force_relayout(self) {
let doc = document_from_node(self).root();
let node: JSRef<Node> = NodeCast::from_ref(self);
doc.r().content_changed(node, NodeDamage::OtherNodeDamage)
}
fn radio_group_updated(self, group: Option<&str>) {
if self.Checked() {
broadcast_radio_checked(self, group);
}
}
fn get_radio_group_name(self) -> Option<String> {
//TODO: determine form owner
let elem: JSRef<Element> = ElementCast::from_ref(self);
elem.get_attribute(ns!(""), &atom!("name"))
.root()
.map(|name| name.r().Value())
}
fn update_checked_state(self, checked: bool, dirty: bool) {
self.checked.set(checked);
if dirty {
self.checked_changed.set(true);
}
if self.input_type.get() == InputType::InputRadio && checked {
broadcast_radio_checked(self,
self.get_radio_group_name()
.as_ref()
.map(|group| group.as_slice()));
}
//TODO: dispatch change event
}
fn get_size(&self) -> u32 {
self.size.get()
}
fn get_indeterminate_state(self) -> bool {
self.indeterminate.get()
}
// https://html.spec.whatwg.org/multipage/forms.html#concept-fe-mutable
fn mutable(self) -> bool {
// https://html.spec.whatwg.org/multipage/forms.html#the-input-element:concept-fe-mutable
// https://html.spec.whatwg.org/multipage/forms.html#the-readonly-attribute:concept-fe-mutable
let node: JSRef<Node> = NodeCast::from_ref(self);
!(node.get_disabled_state() || self.ReadOnly())
}
// https://html.spec.whatwg.org/multipage/forms.html#the-input-element:concept-form-reset-control
fn reset(self) {
match self.input_type.get() {
InputType::InputRadio | InputType::InputCheckbox => {
self.update_checked_state(self.DefaultChecked(), false);
self.checked_changed.set(false);
},
InputType::InputImage => (),
_ => ()
}
self.SetValue(self.DefaultValue());
self.value_changed.set(false);
self.force_relayout();
}
}
impl<'a> VirtualMethods for JSRef<'a, HTMLInputElement> {
fn super_type<'b>(&'b self) -> Option<&'b VirtualMethods> {
let htmlelement: &JSRef<HTMLElement> = HTMLElementCast::from_borrowed_ref(self);
Some(htmlelement as &VirtualMethods)
}
fn after_set_attr(&self, attr: JSRef<Attr>) {
if let Some(ref s) = self.super_type() {
s.after_set_attr(attr);
}
match attr.local_name() {
&atom!("disabled") => {
let node: JSRef<Node> = NodeCast::from_ref(*self);
node.set_disabled_state(true);
node.set_enabled_state(false);
}
&atom!("checked") => {
// https://html.spec.whatwg.org/multipage/forms.html#the-input-element:concept-input-checked-dirty
if !self.checked_changed.get() {
self.update_checked_state(true, false);
}
}
&atom!("size") => {
match *attr.value() {
AttrValue::UInt(_, value) => self.size.set(value),
_ => panic!("Expected an AttrValue::UInt"),
}
}
&atom!("type") => {
let value = attr.value();
self.input_type.set(match value.as_slice() {
"button" => InputType::InputButton,
"submit" => InputType::InputSubmit,
"reset" => InputType::InputReset,
"file" => InputType::InputFile,
"radio" => InputType::InputRadio,
"checkbox" => InputType::InputCheckbox,
"password" => InputType::InputPassword,
_ => InputType::InputText,
});
if self.input_type.get() == InputType::InputRadio {
self.radio_group_updated(self.get_radio_group_name()
.as_ref()
.map(|group| group.as_slice()));
}
}
&atom!("value") => {
if !self.value_changed.get() {
self.textinput.borrow_mut().set_content(attr.value().as_slice().to_owned());
}
}
&atom!("name") => {
if self.input_type.get() == InputType::InputRadio {
let value = attr.value();
self.radio_group_updated(Some(value.as_slice()));
}
}
_ if attr.local_name() == &Atom::from_slice("placeholder") => {
let value = attr.value();
let stripped = value.as_slice().chars()
.filter(|&c| c != '\n' && c != '\r')
.collect::<String>();
*self.placeholder.borrow_mut() = stripped;
}
_ => ()
}
}
fn before_remove_attr(&self, attr: JSRef<Attr>) {
if let Some(ref s) = self.super_type() {
s.before_remove_attr(attr);
}
match attr.local_name() {
&atom!("disabled") => {
let node: JSRef<Node> = NodeCast::from_ref(*self);
node.set_disabled_state(false);
node.set_enabled_state(true);
node.check_ancestors_disabled_state_for_form_control();
}
&atom!("checked") => {
// https://html.spec.whatwg.org/multipage/forms.html#the-input-element:concept-input-checked-dirty
if !self.checked_changed.get() {
self.update_checked_state(false, false);
}
}
&atom!("size") => {
self.size.set(DEFAULT_INPUT_SIZE);
}
&atom!("type") => {
if self.input_type.get() == InputType::InputRadio {
broadcast_radio_checked(*self,
self.get_radio_group_name()
.as_ref()
.map(|group| group.as_slice()));
}
self.input_type.set(InputType::InputText);
}
&atom!("value") => {
if !self.value_changed.get() {
self.textinput.borrow_mut().set_content("".to_owned());
}
}
&atom!("name") => {
if self.input_type.get() == InputType::InputRadio {
self.radio_group_updated(None);
}
}
_ if attr.local_name() == &Atom::from_slice("placeholder") => {
self.placeholder.borrow_mut().clear();
}
_ => ()
}
}
fn parse_plain_attribute(&self, name: &Atom, value: DOMString) -> AttrValue {
match name {
&atom!("size") => AttrValue::from_u32(value, DEFAULT_INPUT_SIZE),
_ => self.super_type().unwrap().parse_plain_attribute(name, value),
}
}
fn bind_to_tree(&self, tree_in_doc: bool) {
if let Some(ref s) = self.super_type() {
s.bind_to_tree(tree_in_doc);
}
let node: JSRef<Node> = NodeCast::from_ref(*self);
node.check_ancestors_disabled_state_for_form_control();
}
fn unbind_from_tree(&self, tree_in_doc: bool) {
if let Some(ref s) = self.super_type() {
s.unbind_from_tree(tree_in_doc);
}
let node: JSRef<Node> = NodeCast::from_ref(*self);
if node.ancestors().any(|ancestor| ancestor.is_htmlfieldsetelement()) {
node.check_ancestors_disabled_state_for_form_control();
} else {
node.check_disabled_attribute();
}
}
fn handle_event(&self, event: JSRef<Event>) {
if let Some(s) = self.super_type() {
s.handle_event(event);
}
if "click" == event.Type().as_slice() && !event.DefaultPrevented() {
match self.input_type.get() {
InputType::InputRadio => self.update_checked_state(true, true),
_ => {}
}
// TODO: Dispatch events for non activatable inputs
// https://html.spec.whatwg.org/multipage/forms.html#common-input-element-events
//TODO: set the editing position for text inputs
let doc = document_from_node(*self).root();
doc.r().request_focus(ElementCast::from_ref(*self));
} else if "keydown" == event.Type().as_slice() && !event.DefaultPrevented() &&
(self.input_type.get() == InputType::InputText ||
self.input_type.get() == InputType::InputPassword) {
let keyevent: Option<JSRef<KeyboardEvent>> = KeyboardEventCast::to_ref(event);
keyevent.map(|keyevent| {
match self.textinput.borrow_mut().handle_keydown(keyevent) {
TriggerDefaultAction => (),
DispatchInput => {
self.value_changed.set(true);
self.force_relayout();
event.PreventDefault();
}
Nothing => (),
}
});
}
}
}
impl<'a> FormControl<'a> for JSRef<'a, HTMLInputElement> {
fn to_element(self) -> JSRef<'a, Element> {
ElementCast::from_ref(self)
}
}
impl<'a> Activatable for JSRef<'a, HTMLInputElement> {
fn as_element(&self) -> Temporary<Element> {
Temporary::from_rooted(ElementCast::from_ref(*self))
}
fn is_instance_activatable(&self) -> bool {
match self.input_type.get() {
// https://html.spec.whatwg.org/multipage/forms.html#submit-button-state-%28type=submit%29:activation-behaviour-2
// https://html.spec.whatwg.org/multipage/forms.html#reset-button-state-%28type=reset%29:activation-behaviour-2
// https://html.spec.whatwg.org/multipage/forms.html#checkbox-state-%28type=checkbox%29:activation-behaviour-2
// https://html.spec.whatwg.org/multipage/forms.html#radio-button-state-%28type=radio%29:activation-behaviour-2
InputType::InputSubmit | InputType::InputReset
| InputType::InputCheckbox | InputType::InputRadio => self.mutable(),
_ => false
}
}
// https://html.spec.whatwg.org/multipage/interaction.html#run-pre-click-activation-steps
#[allow(unsafe_code)]
fn pre_click_activation(&self) {
let mut cache = self.activation_state.borrow_mut();
let ty = self.input_type.get();
cache.old_type = ty;
cache.was_mutable = self.mutable();
if cache.was_mutable {
match ty {
// https://html.spec.whatwg.org/multipage/forms.html#submit-button-state-(type=submit):activation-behavior
// InputType::InputSubmit => (), // No behavior defined
// https://html.spec.whatwg.org/multipage/forms.html#reset-button-state-(type=reset):activation-behavior
// InputType::InputSubmit => (), // No behavior defined
InputType::InputCheckbox => {
// https://html.spec.whatwg.org/multipage/forms.html#checkbox-state-(type=checkbox):pre-click-activation-steps
// cache current values of `checked` and `indeterminate`
// we may need to restore them later
cache.indeterminate = self.Indeterminate();
cache.checked = self.Checked();
cache.checked_changed = self.checked_changed.get();
self.SetIndeterminate(false);
self.SetChecked(!cache.checked);
},
// https://html.spec.whatwg.org/multipage/forms.html#radio-button-state-(type=radio):pre-click-activation-steps
InputType::InputRadio => {
//TODO: if not in document, use root ancestor instead of document
let owner = self.form_owner().root();
let doc = document_from_node(*self).root();
let doc_node: JSRef<Node> = NodeCast::from_ref(doc.r());
let group = self.get_radio_group_name();;
// Safe since we only manipulate the DOM tree after finding an element
let checked_member = unsafe {
doc_node.query_selector_iter("input[type=radio]".to_owned()).unwrap()
.filter_map(|t| HTMLInputElementCast::to_ref(t))
.filter(|&r| in_same_group(r, owner.r(),
group.as_ref().map(|gr| gr.as_slice())))
.find(|r| r.Checked())
};
cache.checked_radio.assign(checked_member);
cache.checked_changed = self.checked_changed.get();
self.SetChecked(true);
}
_ => ()
}
}
}
// https://html.spec.whatwg.org/multipage/interaction.html#run-canceled-activation-steps
fn canceled_activation(&self) {
let cache = self.activation_state.borrow();
let ty = self.input_type.get();
if cache.old_type != ty {
// Type changed, abandon ship
// https://www.w3.org/Bugs/Public/show_bug.cgi?id=27414
return;
}
match ty {
// https://html.spec.whatwg.org/multipage/forms.html#submit-button-state-(type=submit):activation-behavior
// InputType::InputSubmit => (), // No behavior defined
// https://html.spec.whatwg.org/multipage/forms.html#reset-button-state-(type=reset):activation-behavior
// InputType::InputReset => (), // No behavior defined
// https://html.spec.whatwg.org/multipage/forms.html#checkbox-state-(type=checkbox):canceled-activation-steps
InputType::InputCheckbox => {
// We want to restore state only if the element had been changed in the first place
if cache.was_mutable {
self.SetIndeterminate(cache.indeterminate);
self.SetChecked(cache.checked);
self.checked_changed.set(cache.checked_changed);
}
},
// https://html.spec.whatwg.org/multipage/forms.html#radio-button-state-(type=radio):canceled-activation-steps
InputType::InputRadio => {
// We want to restore state only if the element had been changed in the first place
if cache.was_mutable {
let old_checked: Option<Root<HTMLInputElement>> = cache.checked_radio.get().root();
let name = self.get_radio_group_name();
match old_checked {
Some(o) => {
// Avoiding iterating through the whole tree here, instead
// we can check if the conditions for radio group siblings apply
if name == o.r().get_radio_group_name() && // TODO should be compatibility caseless
self.form_owner() == o.r().form_owner() &&
// TODO Both a and b are in the same home subtree
o.r().input_type.get() == InputType::InputRadio {
o.r().SetChecked(true);
} else {
self.SetChecked(false);
}
},
None => self.SetChecked(false)
};
self.checked_changed.set(cache.checked_changed);
}
}
_ => ()
}
}
// https://html.spec.whatwg.org/multipage/interaction.html#run-post-click-activation-steps
fn activation_behavior(&self) {
let ty = self.input_type.get();
if self.activation_state.borrow().old_type != ty {
// Type changed, abandon ship
// https://www.w3.org/Bugs/Public/show_bug.cgi?id=27414
return;
}
match ty {
InputType::InputSubmit => {
// https://html.spec.whatwg.org/multipage/forms.html#submit-button-state-(type=submit):activation-behavior
// FIXME (Manishearth): support document owners (needs ability to get parent browsing context)
if self.mutable() /* and document owner is fully active */ {
self.form_owner().map(|o| {
o.root().r().submit(SubmittedFrom::NotFromFormSubmitMethod,
FormSubmitter::InputElement(self.clone()))
});
}
},
InputType::InputReset => {
// https://html.spec.whatwg.org/multipage/forms.html#reset-button-state-(type=reset):activation-behavior
// FIXME (Manishearth): support document owners (needs ability to get parent browsing context)
if self.mutable() /* and document owner is fully active */ {
self.form_owner().map(|o| {
o.root().r().reset(ResetFrom::NotFromFormResetMethod)
});
}
},
InputType::InputCheckbox | InputType::InputRadio => {
// https://html.spec.whatwg.org/multipage/forms.html#checkbox-state-(type=checkbox):activation-behavior
// https://html.spec.whatwg.org/multipage/forms.html#radio-button-state-(type=radio):activation-behavior
if self.mutable() {
let win = window_from_node(*self).root();
let event = Event::new(GlobalRef::Window(win.r()),
"input".to_owned(),
EventBubbles::Bubbles,
EventCancelable::NotCancelable).root();
let target: JSRef<EventTarget> = EventTargetCast::from_ref(*self);
event.r().fire(target);
let event = Event::new(GlobalRef::Window(win.r()),
"change".to_owned(),
EventBubbles::Bubbles,
EventCancelable::NotCancelable).root();
let target: JSRef<EventTarget> = EventTargetCast::from_ref(*self);
event.r().fire(target);
}
},
_ => ()
}
}
// https://html.spec.whatwg.org/multipage/forms.html#implicit-submission
#[allow(unsafe_code)]
fn implicit_submission(&self, ctrlKey: bool, shiftKey: bool, altKey: bool, metaKey: bool) {
let doc = document_from_node(*self).root();
let node: JSRef<Node> = NodeCast::from_ref(doc.r());
let owner = self.form_owner();
let elem: JSRef<Element> = ElementCast::from_ref(*self);
if owner.is_none() || elem.click_in_progress() {
return;
}
// This is safe because we are stopping after finding the first element
// and only then performing actions which may modify the DOM tree
unsafe {
node.query_selector_iter("input[type=submit]".to_owned()).unwrap()
.filter_map(|t| {
let h: Option<JSRef<HTMLInputElement>> = HTMLInputElementCast::to_ref(t);
h
})
.find(|r| r.form_owner() == owner)
.map(|s| s.synthetic_click_activation(ctrlKey, shiftKey, altKey, metaKey));
}
}
}