Skip to content

Commit 8ae44b3

Browse files
Implement form submission
1 parent b5ed723 commit 8ae44b3

File tree

16 files changed

+720
-30
lines changed

16 files changed

+720
-30
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ image = { version = "0.25", default-features = false }
7474
woff = "0.3"
7575
woff2 = "0.3"
7676
html-escape = "0.2.13"
77+
percent-encoding = "2.3.1"
7778

7879
# Other dependencies
7980
rustc-hash = "1.1.0"

apps/readme/src/main.rs

+5-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ mod readme_application;
33

44
use blitz_html::HtmlDocument;
55
use blitz_net::Provider;
6-
use blitz_traits::navigation::NavigationProvider;
6+
use blitz_traits::navigation::{NavigationOptions, NavigationProvider};
77
use markdown::{markdown_to_html, BLITZ_MD_STYLES, GITHUB_MD_STYLES};
88
use notify::{Error as NotifyError, Event as NotifyEvent, RecursiveMode, Watcher as _};
99
use readme_application::{ReadmeApplication, ReadmeEvent};
@@ -27,8 +27,10 @@ struct ReadmeNavigationProvider {
2727
}
2828

2929
impl NavigationProvider for ReadmeNavigationProvider {
30-
fn navigate_new_page(&self, url: String) {
31-
let _ = self.proxy.send_event(BlitzShellEvent::Navigate(url));
30+
fn navigate_to(&self, opts: NavigationOptions) {
31+
let _ = self
32+
.proxy
33+
.send_event(BlitzShellEvent::Navigate(Box::new(opts)));
3234
}
3335
}
3436

apps/readme/src/readme_application.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,8 @@ impl ApplicationHandler<BlitzShellEvent> for ReadmeApplication {
130130
self.reload_document(true);
131131
}
132132
}
133-
BlitzShellEvent::Navigate(url) => {
134-
self.raw_url = url;
133+
BlitzShellEvent::Navigate(opts) => {
134+
self.raw_url = opts.url.into();
135135
self.reload_document(false);
136136
}
137137
event => self.inner.user_event(event_loop, event),

packages/blitz-dom/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ usvg = { workspace = true, optional = true }
5050
woff = { workspace = true, optional = true }
5151
woff2 = { workspace = true, optional = true }
5252
html-escape = { workspace = true }
53+
percent-encoding = { workspace = true }
5354

5455
# IO & Networking
5556
url = { workspace = true, features = ["serde"] }

packages/blitz-dom/src/document.rs

+4
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ pub struct BaseDocument {
126126

127127
pub changed: HashSet<usize>,
128128

129+
pub controls_to_form: HashMap<usize, usize>,
130+
129131
/// Network provider. Can be used to fetch assets.
130132
pub net_provider: SharedProvider<Resource>,
131133

@@ -211,6 +213,7 @@ impl BaseDocument {
211213
focus_node_id: None,
212214
active_node_id: None,
213215
changed: HashSet::new(),
216+
controls_to_form: HashMap::new(),
214217
net_provider: Arc::new(DummyNetProvider::default()),
215218
navigation_provider: Arc::new(DummyNavigationProvider {}),
216219
};
@@ -628,6 +631,7 @@ impl BaseDocument {
628631
Resource::Font(bytes) => {
629632
self.font_ctx.collection.register_fonts(bytes.to_vec());
630633
}
634+
_ => {}
631635
}
632636
}
633637

packages/blitz-dom/src/events/keyboard.rs

+54-8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::{
44
};
55
use blitz_traits::BlitzKeyEvent;
66
use keyboard_types::{Key, Modifiers};
7+
use markup5ever::local_name;
78
use parley::{FontContext, LayoutContext};
89

910
pub(crate) fn handle_keypress(doc: &mut BaseDocument, target: usize, event: BlitzKeyEvent) {
@@ -13,14 +14,18 @@ pub(crate) fn handle_keypress(doc: &mut BaseDocument, target: usize, event: Blit
1314
}
1415

1516
let node = &mut doc.nodes[node_id];
16-
let text_input_data = node
17-
.data
18-
.downcast_element_mut()
19-
.and_then(|el| el.text_input_data_mut());
17+
let Some(element_data) = node.element_data_mut() else {
18+
return;
19+
};
2020

21-
if let Some(input_data) = text_input_data {
21+
if let Some(input_data) = element_data.text_input_data_mut() {
2222
println!("Sent text event to {}", node_id);
23-
apply_keypress_event(input_data, &mut doc.font_ctx, &mut doc.layout_ctx, event);
23+
let implicit_submission =
24+
apply_keypress_event(input_data, &mut doc.font_ctx, &mut doc.layout_ctx, event);
25+
26+
if implicit_submission {
27+
implicit_form_submission(doc, target);
28+
}
2429
}
2530
}
2631
}
@@ -35,10 +40,10 @@ pub(crate) fn apply_keypress_event(
3540
font_ctx: &mut FontContext,
3641
layout_ctx: &mut LayoutContext<TextBrush>,
3742
event: BlitzKeyEvent,
38-
) {
43+
) -> bool {
3944
// Do nothing if it is a keyup event
4045
if !event.state.is_pressed() {
41-
return;
46+
return false;
4247
}
4348

4449
let mods = event.modifiers;
@@ -165,9 +170,50 @@ pub(crate) fn apply_keypress_event(
165170
Key::Enter => {
166171
if is_multiline {
167172
driver.insert_or_replace_selection("\n");
173+
} else {
174+
return true;
168175
}
169176
}
170177
Key::Character(s) => driver.insert_or_replace_selection(&s),
171178
_ => {}
172179
};
180+
false
181+
}
182+
183+
/// https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#field-that-blocks-implicit-submission
184+
fn implicit_form_submission(doc: &BaseDocument, text_target: usize) {
185+
let Some(form_owner_id) = doc.controls_to_form.get(&text_target) else {
186+
return;
187+
};
188+
if doc
189+
.controls_to_form
190+
.iter()
191+
.filter(|(_control_id, form_id)| *form_id == form_owner_id)
192+
.filter_map(|(control_id, _)| doc.nodes[*control_id].element_data())
193+
.filter(|element_data| {
194+
element_data.attr(local_name!("type")).is_some_and(|t| {
195+
matches!(
196+
t,
197+
"text"
198+
| "search"
199+
| "email"
200+
| "url"
201+
| "tel"
202+
| "password"
203+
| "date"
204+
| "month"
205+
| "week"
206+
| "time"
207+
| "datetime-local"
208+
| "number"
209+
)
210+
})
211+
})
212+
.count()
213+
> 1
214+
{
215+
return;
216+
}
217+
218+
doc.submit_form(*form_owner_id, *form_owner_id);
173219
}

packages/blitz-dom/src/events/mouse.rs

+20-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use blitz_traits::{HitResult, MouseEventButtons};
1+
use blitz_traits::{navigation::NavigationOptions, HitResult, MouseEventButtons};
22
use markup5ever::local_name;
33

44
use crate::{node::NodeSpecificData, util::resolve_url, BaseDocument, Node};
@@ -99,10 +99,14 @@ pub(crate) fn handle_click(doc: &mut BaseDocument, _target: usize, x: f32, y: f3
9999
let mut maybe_hit = doc.hit(x, y);
100100

101101
while let Some(hit) = maybe_hit {
102-
let node = &mut doc.nodes[hit.node_id];
102+
let node_id = hit.node_id;
103+
let maybe_element = {
104+
let node = &mut doc.nodes[node_id];
105+
node.data.downcast_element_mut()
106+
};
103107

104-
let Some(el) = node.data.downcast_element_mut() else {
105-
maybe_hit = parent_hit(node, x, y);
108+
let Some(el) = maybe_element else {
109+
maybe_hit = parent_hit(&doc.nodes[node_id], x, y);
106110
continue;
107111
};
108112

@@ -117,20 +121,18 @@ pub(crate) fn handle_click(doc: &mut BaseDocument, _target: usize, x: f32, y: f3
117121
&& matches!(el.attr(local_name!("type")), Some("checkbox"))
118122
{
119123
BaseDocument::toggle_checkbox(el);
120-
doc.set_focus_to(hit.node_id);
124+
doc.set_focus_to(node_id);
121125
return;
122126
} else if el.name.local == local_name!("input")
123127
&& matches!(el.attr(local_name!("type")), Some("radio"))
124128
{
125-
let node_id = node.id;
126129
let radio_set = el.attr(local_name!("name")).unwrap().to_string();
127130
BaseDocument::toggle_radio(doc, radio_set, node_id);
128-
BaseDocument::set_focus_to(doc, hit.node_id);
131+
BaseDocument::set_focus_to(doc, node_id);
129132
return;
130133
}
131134
// Clicking labels triggers click, and possibly input event, of associated input
132135
else if el.name.local == local_name!("label") {
133-
let node_id = node.id;
134136
if let Some(target_node_id) = doc
135137
.label_bound_input_elements(node_id)
136138
.first()
@@ -146,7 +148,8 @@ pub(crate) fn handle_click(doc: &mut BaseDocument, _target: usize, x: f32, y: f3
146148
} else if el.name.local == local_name!("a") {
147149
if let Some(href) = el.attr(local_name!("href")) {
148150
if let Some(url) = resolve_url(&doc.base_url, href) {
149-
doc.navigation_provider.navigate_new_page(url.into());
151+
doc.navigation_provider
152+
.navigate_to(NavigationOptions::new(url, doc.id()));
150153
} else {
151154
println!(
152155
"{href} is not parseable as a url. : {base_url:?}",
@@ -157,10 +160,17 @@ pub(crate) fn handle_click(doc: &mut BaseDocument, _target: usize, x: f32, y: f3
157160
} else {
158161
println!("Clicked link without href: {:?}", el.attrs());
159162
}
163+
} else if el.name.local == local_name!("input")
164+
&& el.attr(local_name!("type")) == Some("submit")
165+
|| el.name.local == local_name!("button")
166+
{
167+
if let Some(form_owner) = doc.controls_to_form.get(&node_id) {
168+
doc.submit_form(*form_owner, node_id);
169+
}
160170
}
161171

162172
// No match. Recurse up to parent.
163-
maybe_hit = parent_hit(&doc.nodes[hit.node_id], x, y)
173+
maybe_hit = parent_hit(&doc.nodes[node_id], x, y)
164174
}
165175

166176
// If nothing is matched then clear focus

0 commit comments

Comments
 (0)