add selection api to HTMLInputElement

This commit is contained in:
Muki Kiboigo
2026-01-19 18:34:02 -08:00
parent 319a1c3367
commit d7015fa3b6
3 changed files with 130 additions and 27 deletions

View File

@@ -2933,16 +2933,7 @@ pub fn handleKeydown(self: *Page, target: *Node, event: *Event) !void {
// Handle printable characters
if (key.isPrintable()) {
// if the input is selected, replace the content.
if (input._selected) {
const new_value = try self.arena.dupe(u8, key.asString());
try input.setValue(new_value, self);
input._selected = false;
return;
}
const current_value = input.getValue();
const new_value = try std.mem.concat(self.arena, u8, &.{ current_value, key.asString() });
try input.setValue(new_value, self);
try input.innerInsert(key.asString(), self);
}
return;
}
@@ -3011,18 +3002,7 @@ pub fn insertText(self: *Page, v: []const u8) !void {
return;
}
// If the input is selected, replace the existing value
if (input._selected) {
const new_value = try self.arena.dupe(u8, v);
try input.setValue(new_value, self);
input._selected = false;
return;
}
// Or append the value
const current_value = input.getValue();
const new_value = try std.mem.concat(self.arena, u8, &.{ current_value, v });
return input.setValue(new_value, self);
try input.innerInsert(v, self);
}
if (html_element.is(Element.Html.TextArea)) |textarea| {

View File

@@ -28,7 +28,7 @@ const Node = @import("Node.zig");
/// https://w3c.github.io/selection-api/
const Selection = @This();
const SelectionDirection = enum { backward, forward, none };
pub const SelectionDirection = enum { backward, forward, none };
_range: ?*Range = null,
_direction: SelectionDirection = .none,

View File

@@ -24,6 +24,7 @@ const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig");
const Form = @import("Form.zig");
const Selection = @import("../../Selection.zig");
const Input = @This();
@@ -74,9 +75,12 @@ _value: ?[]const u8 = null,
_checked: bool = false,
_checked_dirty: bool = false,
_input_type: Type = .text,
_selected: bool = false,
_indeterminate: bool = false,
_selection_start: u32 = 0,
_selection_end: u32 = 0,
_selection_direction: Selection.SelectionDirection = .none,
pub fn asElement(self: *Input) *Element {
return self._proto._proto;
}
@@ -255,8 +259,120 @@ pub fn setRequired(self: *Input, required: bool, page: *Page) !void {
}
}
pub fn select(self: *Input) void {
self._selected = true;
pub fn select(self: *Input) !void {
const len = if (self._value) |v| @as(u32, @intCast(v.len)) else 0;
try self.setSelectionRange(0, len, null);
}
fn selectionAvailable(self: *const Input) bool {
switch (self._input_type) {
.text, .search, .url, .tel, .password => return true,
else => return false,
}
}
const HowSelected = union(enum) { partial: struct { u32, u32 }, full, none };
fn howSelected(self: *const Input) HowSelected {
if (!self.selectionAvailable()) return .none;
const value = self._value orelse return .none;
if (self._selection_start == self._selection_end) return .none;
if (self._selection_start == 0 and self._selection_end == value.len) return .full;
return .{ .partial = .{ self._selection_start, self._selection_end } };
}
pub fn innerInsert(self: *Input, str: []const u8, page: *Page) !void {
const arena = page.arena;
switch (self.howSelected()) {
.full => {
// if the input is fully selected, replace the content.
const new_value = try arena.dupe(u8, str);
try self.setValue(new_value, page);
self._selection_start = 1;
self._selection_end = 1;
self._selection_direction = .none;
},
.partial => |range| {
// if the input is partially selected, replace the selected content.
const current_value = self.getValue();
const before = current_value[0..range[0]];
const remaining = current_value[range[1]..];
const new_value = try std.mem.concat(
arena,
u8,
&.{ before, str, remaining },
);
try self.setValue(new_value, page);
const new_pos = range[0] + str.len;
self._selection_start = @intCast(new_pos);
self._selection_end = @intCast(new_pos);
self._selection_direction = .none;
},
.none => {
// if the input is not selected, just insert at cursor.
const current_value = self.getValue();
const new_value = try std.mem.concat(arena, u8, &.{ current_value, str });
try self.setValue(new_value, page);
},
}
}
pub fn getSelectionDirection(self: *const Input) []const u8 {
return @tagName(self._selection_direction);
}
pub fn getSelectionStart(self: *const Input) !?u32 {
if (!self.selectionAvailable()) return null;
return self._selection_start;
}
pub fn setSelectionStart(self: *Input, value: u32) !void {
if (!self.selectionAvailable()) return error.InvalidStateError;
self._selection_start = value;
}
pub fn getSelectionEnd(self: *const Input) !?u32 {
if (!self.selectionAvailable()) return null;
return self._selection_end;
}
pub fn setSelectionEnd(self: *Input, value: u32) !void {
if (!self.selectionAvailable()) return error.InvalidStateError;
self._selection_end = value;
}
pub fn setSelectionRange(self: *Input, selection_start: u32, selection_end: u32, selection_dir: ?[]const u8) !void {
if (!self.selectionAvailable()) return error.InvalidStateError;
const direction = blk: {
if (selection_dir) |sd| {
break :blk std.meta.stringToEnum(Selection.SelectionDirection, sd) orelse .none;
} else break :blk .none;
};
const value = self._value orelse {
self._selection_start = 0;
self._selection_end = 0;
self._selection_direction = .none;
return;
};
const len_u32: u32 = @intCast(value.len);
var start: u32 = if (selection_start > len_u32) len_u32 else selection_start;
const end: u32 = if (selection_end > len_u32) len_u32 else selection_end;
// If end is less than start, both are equal to end.
if (end < start) {
start = end;
}
self._selection_direction = direction;
self._selection_start = start;
self._selection_end = end;
}
pub fn getForm(self: *Input, page: *Page) ?*Form {
@@ -352,6 +468,11 @@ pub const JsApi = struct {
pub const form = bridge.accessor(Input.getForm, null, .{});
pub const indeterminate = bridge.accessor(Input.getIndeterminate, Input.setIndeterminate, .{});
pub const select = bridge.function(Input.select, .{});
pub const selectionStart = bridge.accessor(Input.getSelectionStart, Input.setSelectionStart, .{});
pub const selectionEnd = bridge.accessor(Input.getSelectionEnd, Input.setSelectionEnd, .{});
pub const selectionDirection = bridge.accessor(Input.getSelectionDirection, null, .{});
pub const setSelectionRange = bridge.function(Input.setSelectionRange, .{ .dom_exception = true });
};
pub const Build = struct {
@@ -422,7 +543,9 @@ pub const Build = struct {
clone._value = source._value;
clone._checked = source._checked;
clone._checked_dirty = source._checked_dirty;
clone._selected = source._selected;
clone._selection_direction = source._selection_direction;
clone._selection_start = source._selection_start;
clone._selection_end = source._selection_end;
clone._indeterminate = source._indeterminate;
}
};