servo: Merge #3642 - Implement extremely basic form submission (from Manishearth:form-submit); r=jdm

This is missing a lot of parts, so it doesn't really make any real-world form submission work yet. It provides a skeleton on which the missing bits can be filled in.

What works:
 - `<input>` elements except for `type=file`
 - GET/POST methods
 - URLencoded `enctype`s (default)
 - Submission via `<form>.submit()` only

Stuff that needs to be done for most simple real-world cases to work:
 - [Working text input](https://github.com/servo/servo/pull/3585)
 - Click handlers for `<submit>`
 - Possibly `<textarea>` support
 - Support for the other two enctypes (#3649)

Todo:
 - Correctly implement [planned navigation](https://html.spec.whatwg.org/multipage/forms.html#planned-navigation) using `TrustedFormAddress`  (#3648)
 - [Correctly implement form owners.](https://github.com/servo/servo/issues/3553) Requires html5ever and some discussion of the spec.
 - `<input type=file>` support
 - Image submit support
 - Browsing contexts/targets
 - Support for non-`<input>` controls
 - Validation (?)
 - Dirname (?)

Source-Repo: https://github.com/servo/servo
Source-Revision: 9dfd5e7fcd2011a411b219e8c45aadc0ecb270bd
This commit is contained in:
Manish Goregaokar 2014-10-11 07:45:39 -06:00
Родитель ec00d8be86
Коммит f32cdad867
15 изменённых файлов: 368 добавлений и 23 удалений

Просмотреть файл

@ -4,18 +4,19 @@
use dom::bindings::codegen::Bindings::HTMLButtonElementBinding;
use dom::bindings::codegen::Bindings::HTMLButtonElementBinding::HTMLButtonElementMethods;
use dom::bindings::codegen::InheritTypes::{HTMLElementCast, NodeCast};
use dom::bindings::codegen::InheritTypes::{ElementCast, HTMLElementCast, NodeCast};
use dom::bindings::codegen::InheritTypes::{HTMLButtonElementDerived, HTMLFieldSetElementDerived};
use dom::bindings::js::{JSRef, Temporary};
use dom::bindings::utils::{Reflectable, Reflector};
use dom::document::Document;
use dom::element::{AttributeHandlers, HTMLButtonElementTypeId};
use dom::element::{AttributeHandlers, Element, HTMLButtonElementTypeId};
use dom::eventtarget::{EventTarget, NodeTargetTypeId};
use dom::htmlelement::HTMLElement;
use dom::node::{DisabledStateHelpers, Node, NodeHelpers, ElementNodeTypeId, window_from_node};
use dom::validitystate::ValidityState;
use dom::virtualmethods::VirtualMethods;
use std::ascii::OwnedStrAsciiExt;
use servo_util::str::DOMString;
use string_cache::Atom;
@ -56,6 +57,20 @@ impl<'a> HTMLButtonElementMethods for JSRef<'a, HTMLButtonElement> {
// http://www.whatwg.org/html/#dom-fe-disabled
make_bool_setter!(SetDisabled, "disabled")
// https://html.spec.whatwg.org/multipage/forms.html#dom-button-type
fn Type(self) -> DOMString {
let elem: JSRef<Element> = ElementCast::from_ref(self);
let ty = elem.get_string_attribute("type").into_ascii_lower();
// https://html.spec.whatwg.org/multipage/forms.html#attr-button-type
match ty.as_slice() {
"reset" | "button" | "menu" => ty,
_ => "submit".to_string()
}
}
// https://html.spec.whatwg.org/multipage/forms.html#dom-button-type
make_setter!(SetType, "type")
}
impl<'a> VirtualMethods for JSRef<'a, HTMLButtonElement> {

Просмотреть файл

@ -2,19 +2,32 @@
* 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::bindings::codegen::Bindings::EventBinding::EventMethods;
use dom::bindings::codegen::Bindings::EventTargetBinding::EventTargetMethods;
use dom::bindings::codegen::Bindings::HTMLFormElementBinding;
use dom::bindings::codegen::Bindings::HTMLFormElementBinding::HTMLFormElementMethods;
use dom::bindings::codegen::InheritTypes::{ElementCast, HTMLFormElementDerived};
use dom::bindings::codegen::Bindings::HTMLInputElementBinding::HTMLInputElementMethods;
use dom::bindings::codegen::InheritTypes::{ElementCast, EventTargetCast, HTMLFormElementDerived, NodeCast};
use dom::bindings::codegen::InheritTypes::HTMLInputElementCast;
use dom::bindings::global::Window;
use dom::bindings::js::{JSRef, Temporary};
use dom::bindings::utils::{Reflectable, Reflector};
use dom::document::Document;
use dom::element::{Element, AttributeHandlers, HTMLFormElementTypeId};
use dom::document::{Document, DocumentHelpers};
use dom::element::{Element, AttributeHandlers, HTMLFormElementTypeId, HTMLTextAreaElementTypeId, HTMLDataListElementTypeId};
use dom::element::{HTMLInputElementTypeId, HTMLButtonElementTypeId, HTMLObjectElementTypeId, HTMLSelectElementTypeId};
use dom::event::Event;
use dom::eventtarget::{EventTarget, NodeTargetTypeId};
use dom::htmlelement::HTMLElement;
use dom::node::{Node, ElementNodeTypeId, window_from_node};
use dom::htmlinputelement::HTMLInputElement;
use dom::node::{Node, NodeHelpers, ElementNodeTypeId, document_from_node, window_from_node};
use http::method::Post;
use servo_msg::constellation_msg::LoadData;
use servo_util::str::DOMString;
use script_task::{ScriptChan, TriggerLoadMsg};
use std::ascii::OwnedStrAsciiExt;
use std::str::StrSlice;
use url::UrlParser;
use url::form_urlencoded::serialize;
#[jstraceable]
#[must_root]
@ -52,7 +65,7 @@ impl<'a> HTMLFormElementMethods for JSRef<'a, HTMLFormElement> {
// https://html.spec.whatwg.org/multipage/forms.html#dom-fs-action
fn Action(self) -> DOMString {
let element: JSRef<Element> = ElementCast::from_ref(self);
let url = element.get_url_attribute("src");
let url = element.get_url_attribute("action");
match url.as_slice() {
"" => {
let window = window_from_node(self).root();
@ -135,6 +148,196 @@ impl<'a> HTMLFormElementMethods for JSRef<'a, HTMLFormElement> {
// https://html.spec.whatwg.org/multipage/forms.html#dom-fs-target
make_setter!(SetTarget, "target")
// https://html.spec.whatwg.org/multipage/forms.html#the-form-element:concept-form-submit
fn Submit(self) {
self.submit(true, FormElement(self));
}
}
pub trait HTMLFormElementHelpers {
// https://html.spec.whatwg.org/multipage/forms.html#concept-form-submit
fn submit(self, from_submit_method: bool, submitter: FormSubmitter);
// https://html.spec.whatwg.org/multipage/forms.html#constructing-the-form-data-set
fn get_form_dataset(self, submitter: Option<FormSubmitter>) -> Vec<FormDatum>;
}
impl<'a> HTMLFormElementHelpers for JSRef<'a, HTMLFormElement> {
fn submit(self, _from_submit_method: bool, submitter: FormSubmitter) {
// Step 1
let doc = document_from_node(self).root();
let win = window_from_node(self).root();
let base = doc.url();
// TODO: Handle browsing contexts
// TODO: Handle validation
let event = Event::new(&Window(*win),
"submit".to_string(),
true, true).root();
let target: JSRef<EventTarget> = EventTargetCast::from_ref(self);
target.DispatchEvent(*event).ok();
if event.DefaultPrevented() {
return;
}
// Step 6
let form_data = self.get_form_dataset(Some(submitter));
// Step 7-8
let mut action = submitter.action();
if action.is_empty() {
action = base.serialize();
}
// TODO: Resolve the url relative to the submitter element
// Step 10-15
let action_components = UrlParser::new().base_url(base).parse(action.as_slice()).unwrap_or(base.clone());
let _action = action_components.serialize();
let scheme = action_components.scheme.clone();
let enctype = submitter.enctype();
let method = submitter.method();
let _target = submitter.target();
// TODO: Handle browsing contexts, partially loaded documents (step 16-17)
let parsed_data = match enctype {
UrlEncoded => serialize(form_data.iter().map(|d| (d.name.as_slice(), d.value.as_slice())), None),
_ => "".to_string() // TODO: Add serializers for the other encoding types
};
let mut load_data = LoadData::new(action_components);
// Step 18
match (scheme.as_slice(), method) {
(_, FormDialog) => return, // Unimplemented
("http", FormGet) | ("https", FormGet) => {
load_data.url.query = Some(parsed_data);
},
("http", FormPost) | ("https", FormPost) => {
load_data.method = Post;
load_data.data = Some(parsed_data.into_bytes());
},
// https://html.spec.whatwg.org/multipage/forms.html#submit-get-action
("ftp", _) | ("javascript", _) | ("data", FormGet) => (),
_ => return // Unimplemented (data and mailto)
}
// This is wrong. https://html.spec.whatwg.org/multipage/forms.html#planned-navigation
let ScriptChan(ref script_chan) = win.script_chan;
script_chan.send(TriggerLoadMsg(win.page.id, load_data));
}
fn get_form_dataset(self, _submitter: Option<FormSubmitter>) -> Vec<FormDatum> {
fn clean_crlf(s: &str) -> DOMString {
// https://html.spec.whatwg.org/multipage/forms.html#constructing-the-form-data-set
// Step 4
let mut buf = "".to_string();
let mut prev = ' ';
for ch in s.chars() {
match ch {
'\n' if prev != '\r' => {
buf.push_char('\r');
buf.push_char('\n');
},
'\n' => {
buf.push_char('\n');
},
// This character isn't LF but is
// preceded by CR
_ if prev == '\r' => {
buf.push_char('\r');
buf.push_char('\n');
buf.push_char(ch);
},
_ => buf.push_char(ch)
};
prev = ch;
}
// In case the last character was CR
if prev == '\r' {
buf.push_char('\n');
}
buf
}
let node: JSRef<Node> = NodeCast::from_ref(self);
// TODO: This is an incorrect way of getting controls owned
// by the form, but good enough until html5ever lands
let mut data_set = node.traverse_preorder().filter_map(|child| {
if child.get_disabled_state() {
return None;
}
if child.ancestors().any(|a| a.type_id() == ElementNodeTypeId(HTMLDataListElementTypeId)) {
return None;
}
// XXXManishearth don't include it if it is a button but not the submitter
match child.type_id() {
ElementNodeTypeId(HTMLInputElementTypeId) => {
let input: JSRef<HTMLInputElement> = HTMLInputElementCast::to_ref(child).unwrap();
let ty = input.Type();
let name = input.Name();
match ty.as_slice() {
"radio" | "checkbox" => {
if !input.Checked() || name.is_empty() {
return None;
}
},
"image" => (),
_ => {
if name.is_empty() {
return None;
}
}
}
let mut value = input.Value();
match ty.as_slice() {
"image" => None, // Unimplemented
"radio" | "checkbox" => {
if value.is_empty() {
value = "on".to_string();
}
Some(FormDatum {
ty: ty,
name: name,
value: value
})
},
"file" => None, // Unimplemented
_ => Some(FormDatum {
ty: ty,
name: name,
value: input.Value()
})
}
}
ElementNodeTypeId(HTMLButtonElementTypeId) => {
// Unimplemented
None
}
ElementNodeTypeId(HTMLSelectElementTypeId) => {
// Unimplemented
None
}
ElementNodeTypeId(HTMLObjectElementTypeId) => {
// Unimplemented
None
}
ElementNodeTypeId(HTMLTextAreaElementTypeId) => {
// Unimplemented
None
}
_ => None
}
});
// TODO: Handle `dirnames` (needs directionality support)
// https://html.spec.whatwg.org/multipage/dom.html#the-directionality
let mut ret: Vec<FormDatum> = data_set.collect();
for mut datum in ret.iter_mut() {
match datum.ty.as_slice() {
"file" | "textarea" => (),
_ => {
datum.name = clean_crlf(datum.name.as_slice());
datum.value = clean_crlf(datum.value.as_slice());
}
}
};
ret
}
}
impl Reflectable for HTMLFormElement {
@ -142,3 +345,67 @@ impl Reflectable for HTMLFormElement {
self.htmlelement.reflector()
}
}
// TODO: add file support
pub struct FormDatum {
pub ty: DOMString,
pub name: DOMString,
pub value: DOMString
}
pub enum FormEncType {
TextPlainEncoded,
UrlEncoded,
FormDataEncoded
}
pub enum FormMethod {
FormGet,
FormPost,
FormDialog
}
pub enum FormSubmitter<'a> {
FormElement(JSRef<'a, HTMLFormElement>)
// TODO: Submit buttons, image submit, etc etc
}
impl<'a> FormSubmitter<'a> {
fn action(&self) -> DOMString {
match *self {
FormElement(form) => form.Action()
}
}
fn enctype(&self) -> FormEncType {
match *self {
FormElement(form) => {
match form.Enctype().as_slice() {
"multipart/form-data" => FormDataEncoded,
"text/plain" => TextPlainEncoded,
// https://html.spec.whatwg.org/multipage/forms.html#attr-fs-enctype
// urlencoded is the default
_ => UrlEncoded
}
}
}
}
fn method(&self) -> FormMethod {
match *self {
FormElement(form) => {
match form.Method().as_slice() {
"dialog" => FormDialog,
"post" => FormPost,
_ => FormGet
}
}
}
}
fn target(&self) -> DOMString {
match *self {
FormElement(form) => form.Target()
}
}
}

Просмотреть файл

@ -24,6 +24,7 @@ use dom::virtualmethods::VirtualMethods;
use servo_util::str::{DOMString, parse_unsigned_integer};
use string_cache::Atom;
use std::ascii::OwnedStrAsciiExt;
use std::cell::{Cell, RefCell};
use std::mem;
@ -120,7 +121,9 @@ impl<'a> HTMLInputElementMethods for JSRef<'a, HTMLInputElement> {
make_bool_setter!(SetDisabled, "disabled")
// https://html.spec.whatwg.org/multipage/forms.html#dom-input-checked
make_bool_getter!(Checked)
fn Checked(self) -> bool {
self.checked.get()
}
// https://html.spec.whatwg.org/multipage/forms.html#dom-input-checked
make_bool_setter!(SetChecked, "checked")
@ -131,8 +134,30 @@ impl<'a> HTMLInputElementMethods for JSRef<'a, HTMLInputElement> {
// 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
fn Type(self) -> DOMString {
let elem: JSRef<Element> = ElementCast::from_ref(self);
let ty = elem.get_string_attribute("type").into_ascii_lower();
// https://html.spec.whatwg.org/multipage/forms.html#attr-input-type
match ty.as_slice() {
"hidden" | "search" | "tel" |
"url" | "email" | "password" |
"datetime" | "date" | "month" |
"week" | "time" | "datetime-local" |
"number" | "range" | "color" |
"checkbox" | "radio" | "file" |
"submit" | "image" | "reset" | "button" => ty,
_ => "text".to_string()
}
}
// 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
make_getter!(Value)
fn Value(self) -> DOMString {
self.value.borrow().clone().unwrap_or("".to_string())
}
// https://html.spec.whatwg.org/multipage/forms.html#dom-input-value
make_setter!(SetValue, "value")

Просмотреть файл

@ -86,6 +86,12 @@ impl<'a> HTMLObjectElementMethods for JSRef<'a, HTMLObjectElement> {
let window = window_from_node(self).root();
ValidityState::new(*window)
}
// https://html.spec.whatwg.org/multipage/embedded-content.html#dom-object-type
make_getter!(Type)
// https://html.spec.whatwg.org/multipage/embedded-content.html#dom-object-type
make_setter!(SetType, "type")
}
impl<'a> VirtualMethods for JSRef<'a, HTMLObjectElement> {

Просмотреть файл

@ -5,13 +5,13 @@
use dom::bindings::codegen::Bindings::HTMLSelectElementBinding;
use dom::bindings::codegen::Bindings::HTMLSelectElementBinding::HTMLSelectElementMethods;
use dom::bindings::codegen::InheritTypes::{HTMLElementCast, NodeCast};
use dom::bindings::codegen::InheritTypes::{HTMLSelectElementDerived, HTMLFieldSetElementDerived};
use dom::bindings::codegen::InheritTypes::{ElementCast, HTMLSelectElementDerived, HTMLFieldSetElementDerived};
use dom::bindings::codegen::UnionTypes::HTMLElementOrLong::HTMLElementOrLong;
use dom::bindings::codegen::UnionTypes::HTMLOptionElementOrHTMLOptGroupElement::HTMLOptionElementOrHTMLOptGroupElement;
use dom::bindings::js::{JSRef, Temporary};
use dom::bindings::utils::{Reflectable, Reflector};
use dom::document::Document;
use dom::element::{AttributeHandlers, HTMLSelectElementTypeId};
use dom::element::{AttributeHandlers, Element, HTMLSelectElementTypeId};
use dom::eventtarget::{EventTarget, NodeTargetTypeId};
use dom::htmlelement::HTMLElement;
use dom::node::{DisabledStateHelpers, Node, NodeHelpers, ElementNodeTypeId, window_from_node};
@ -62,6 +62,16 @@ impl<'a> HTMLSelectElementMethods for JSRef<'a, HTMLSelectElement> {
// http://www.whatwg.org/html/#dom-fe-disabled
make_bool_setter!(SetDisabled, "disabled")
// https://html.spec.whatwg.org/multipage/forms.html#dom-select-type
fn Type(self) -> DOMString {
let elem: JSRef<Element> = ElementCast::from_ref(self);
if elem.has_attribute("multiple") {
"select-multiple".to_string()
} else {
"select-one".to_string()
}
}
}
impl<'a> VirtualMethods for JSRef<'a, HTMLSelectElement> {

Просмотреть файл

@ -50,6 +50,11 @@ impl<'a> HTMLTextAreaElementMethods for JSRef<'a, HTMLTextAreaElement> {
// http://www.whatwg.org/html/#dom-fe-disabled
make_bool_setter!(SetDisabled, "disabled")
// https://html.spec.whatwg.org/multipage/forms.html#dom-textarea-type
fn Type(self) -> DOMString {
"textarea".to_string()
}
}
impl<'a> VirtualMethods for JSRef<'a, HTMLTextAreaElement> {

Просмотреть файл

@ -14,7 +14,7 @@ interface HTMLButtonElement : HTMLElement {
// attribute boolean formNoValidate;
// attribute DOMString formTarget;
// attribute DOMString name;
// attribute DOMString type;
attribute DOMString type;
// attribute DOMString value;
// attribute HTMLMenuElement? menu;

Просмотреть файл

@ -21,7 +21,7 @@ interface HTMLFormElement : HTMLElement {
//getter Element (unsigned long index);
//getter (RadioNodeList or Element) (DOMString name);
//void submit();
void submit();
//void reset();
//boolean checkValidity();
//boolean reportValidity();

Просмотреть файл

@ -37,7 +37,7 @@ interface HTMLInputElement : HTMLElement {
attribute unsigned long size;
// attribute DOMString src;
// attribute DOMString step;
// attribute DOMString type; //XXXjdm need binaryName
attribute DOMString type;
// attribute DOMString defaultValue;
[TreatNullAs=EmptyString] attribute DOMString value;
// attribute Date? valueAsDate;

Просмотреть файл

@ -6,7 +6,7 @@
// http://www.whatwg.org/html/#htmlobjectelement
interface HTMLObjectElement : HTMLElement {
// attribute DOMString data;
// attribute DOMString type;
attribute DOMString type;
// attribute boolean typeMustMatch;
// attribute DOMString name;
// attribute DOMString useMap;

Просмотреть файл

@ -13,7 +13,7 @@ interface HTMLSelectElement : HTMLElement {
// attribute boolean required;
// attribute unsigned long size;
//readonly attribute DOMString type;
readonly attribute DOMString type;
//readonly attribute HTMLOptionsCollection options;
// attribute unsigned long length;

Просмотреть файл

@ -21,7 +21,7 @@ interface HTMLTextAreaElement : HTMLElement {
// attribute unsigned long rows;
// attribute DOMString wrap;
//readonly attribute DOMString type;
readonly attribute DOMString type;
// attribute DOMString defaultValue;
//[TreatNullAs=EmptyString] attribute DOMString value;
//readonly attribute unsigned long textLength;

Просмотреть файл

@ -24,6 +24,7 @@ use script_task::{ExitWindowMsg, FireTimerMsg, ScriptChan, TriggerLoadMsg, Trigg
use script_traits::ScriptControlChan;
use servo_msg::compositor_msg::ScriptListener;
use servo_msg::constellation_msg::LoadData;
use servo_net::image_cache_task::ImageCacheTask;
use servo_util::str::{DOMString,HTML_SPACE_CHARACTERS};
use servo_util::task::{spawn_named};
@ -432,7 +433,7 @@ impl<'a> WindowHelpers for JSRef<'a, Window> {
if href.as_slice().starts_with("#") {
script_chan.send(TriggerFragmentMsg(self.page.id, url));
} else {
script_chan.send(TriggerLoadMsg(self.page.id, url));
script_chan.send(TriggerLoadMsg(self.page.id, LoadData::new(url)));
}
}

Просмотреть файл

@ -81,7 +81,7 @@ pub enum ScriptMsg {
TriggerFragmentMsg(PipelineId, Url),
/// Begins a content-initiated load on the specified pipeline (only
/// dispatched to ScriptTask).
TriggerLoadMsg(PipelineId, Url),
TriggerLoadMsg(PipelineId, LoadData),
/// Instructs the script task to send a navigate message to
/// the constellation (only dispatched to ScriptTask).
NavigateMsg(NavigationDirection),
@ -494,7 +494,7 @@ impl ScriptTask {
// TODO(tkuehn) need to handle auxiliary layouts for iframes
FromConstellation(AttachLayoutMsg(_)) => fail!("should have handled AttachLayoutMsg already"),
FromConstellation(LoadMsg(id, load_data)) => self.load(id, load_data),
FromScript(TriggerLoadMsg(id, url)) => self.trigger_load(id, url),
FromScript(TriggerLoadMsg(id, load_data)) => self.trigger_load(id, load_data),
FromScript(TriggerFragmentMsg(id, url)) => self.trigger_fragment(id, url),
FromConstellation(SendEventMsg(id, event)) => self.handle_event(id, event),
FromScript(FireTimerMsg(id, timer_id)) => self.handle_fire_timer_msg(id, timer_id),
@ -1067,9 +1067,9 @@ impl ScriptTask {
/// The entry point for content to notify that a new load has been requested
/// for the given pipeline.
fn trigger_load(&self, pipeline_id: PipelineId, url: Url) {
fn trigger_load(&self, pipeline_id: PipelineId, load_data: LoadData) {
let ConstellationChan(ref const_chan) = self.constellation_chan;
const_chan.send(LoadUrlMsg(pipeline_id, LoadData::new(url)));
const_chan.send(LoadUrlMsg(pipeline_id, load_data));
}
/// The entry point for content to notify that a fragment url has been requested

Просмотреть файл

@ -0,0 +1,16 @@
<html>
<head></head>
<body>
<!-- Run with nc -l 8000 -->
<form action="http://localhost:8000" method=get id="foo">
<input name=bar type=checkbox checked>
<input name=baz value="baz1" type=radio checked>
<input name=baz value="baz2" type=radio>
<input type=text name=bye value="hi!">
</form>
<script>
// setTimeout because https://github.com/servo/servo/issues/3628
setTimeout(function(){document.getElementById("foo").submit()},5000)
</script>
</body>
</html>