diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index 389cb08b..640d6bf5 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -262,6 +262,8 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled: // and default actions execute (unless prevented) defer { event._event_phase = .none; + event._stop_propagation = false; + event._stop_immediate_propagation = false; // Handle checkbox/radio activation rollback or commit if (activation_state) |state| { state.restore(event, page); @@ -322,19 +324,18 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled: var i: usize = path_len; while (i > 1) { i -= 1; + if (event._stop_propagation) return; const current_target = path[i]; if (self.lookup.get(.{ .event_target = @intFromPtr(current_target), .type_string = event._type_string, })) |list| { try self.dispatchPhase(list, current_target, event, was_handled, true); - if (event._stop_propagation) { - return; - } } } // Phase 2: At target + if (event._stop_propagation) return; event._event_phase = .at_target; const target_et = target.asEventTarget(); @@ -375,14 +376,12 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled: if (event._bubbles) { event._event_phase = .bubbling_phase; for (path[1..]) |current_target| { + if (event._stop_propagation) break; if (self.lookup.get(.{ .type_string = event._type_string, .event_target = @intFromPtr(current_target), })) |list| { try self.dispatchPhase(list, current_target, event, was_handled, false); - if (event._stop_propagation) { - break; - } } } } diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index 569e5f70..1ab895f5 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -129,6 +129,8 @@ pub fn initEvent( self._bubbles = bubbles orelse false; self._cancelable = cancelable orelse false; self._stop_propagation = false; + self._stop_immediate_propagation = false; + self._prevent_default = false; } pub fn deinit(self: *Event, shutdown: bool) void { diff --git a/src/browser/webapi/element/html/Input.zig b/src/browser/webapi/element/html/Input.zig index fcbb05bb..ebc41a2c 100644 --- a/src/browser/webapi/element/html/Input.zig +++ b/src/browser/webapi/element/html/Input.zig @@ -125,7 +125,10 @@ pub fn setType(self: *Input, typ: []const u8, page: *Page) !void { } pub fn getValue(self: *const Input) []const u8 { - return self._value orelse self._default_value orelse ""; + return self._value orelse self._default_value orelse switch (self._input_type) { + .checkbox, .radio => "on", + else => "", + }; } pub fn setValue(self: *Input, value: []const u8, page: *Page) !void { @@ -526,10 +529,19 @@ fn sanitizeValue(self: *Input, value: []const u8, page: *Page) ![]const u8 { }, .color => { if (value.len == 7 and value[0] == '#') { + var needs_lower = false; for (value[1..]) |c| { if (!std.ascii.isHex(c)) return "#000000"; + if (c >= 'A' and c <= 'F') needs_lower = true; } - return value; + if (!needs_lower) return value; + // Normalize to lowercase per spec + const result = try page.call_arena.alloc(u8, 7); + result[0] = '#'; + for (value[1..], 0..) |c, j| { + result[j + 1] = std.ascii.toLower(c); + } + return result; } return "#000000"; }, @@ -639,8 +651,6 @@ pub const Build = struct { self._default_value = element.getAttributeSafe(comptime .wrap("value")); self._default_checked = element.getAttributeSafe(comptime .wrap("checked")) != null; - // Current state starts equal to default - self._value = self._default_value; self._checked = self._default_checked; self._input_type = if (element.getAttributeSafe(comptime .wrap("type"))) |type_attr| @@ -648,6 +658,20 @@ pub const Build = struct { else .text; + // Current value starts equal to default, but sanitized per input type. + // sanitizeValue allocates temporaries from call_arena, so we must + // persist any new buffer into page.arena for the value to survive. + if (self._default_value) |dv| { + const sanitized = try self.sanitizeValue(dv, page); + if (sanitized.ptr == dv.ptr and sanitized.len == dv.len) { + self._value = self._default_value; + } else { + self._value = try page.arena.dupe(u8, sanitized); + } + } else { + self._value = null; + } + // If this is a checked radio button, uncheck others in its group if (self._checked and self._input_type == .radio) { try self.uncheckRadioGroup(page);