From 7ef384c63631b84371ae2adbcfeb493f0fe36ff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A0=20Arrufat?= Date: Tue, 1 Sep 2026 10:13:35 +0200 Subject: [PATCH] webapi: reflect HTMLElement.contentEditable Only isContentEditable existed, so el.contentEditable = 'true' created an expando and never set the attribute. isContentEditable is unchanged. --- .../tests/element/html/contenteditable.html | 19 +++++++++++++++ src/browser/webapi/element/Html.zig | 23 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/browser/tests/element/html/contenteditable.html b/src/browser/tests/element/html/contenteditable.html index eca1ec2bb..cee1277d5 100644 --- a/src/browser/tests/element/html/contenteditable.html +++ b/src/browser/tests/element/html/contenteditable.html @@ -58,3 +58,22 @@ testing.expectEqual(false, el.isContentEditable); } + + diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index 72cbc3387..d1a622ba6 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -611,6 +611,28 @@ pub fn setTitle(self: *HtmlElement, value: []const u8, frame: *Frame) !void { // // "contenteditable" is 15 bytes — past the comptime SSO limit — so the // String wrap runs at runtime, mirroring the pattern in interactive.zig. +/// Reflects the attribute only; `isContentEditable` stays false regardless. +pub fn getContentEditable(self: *HtmlElement) []const u8 { + const raw = self.asElement().getAttributeSafe(.wrap("contenteditable")) orelse return "inherit"; + if (raw.len == 0 or std.ascii.eqlIgnoreCase(raw, "true")) return "true"; + if (std.ascii.eqlIgnoreCase(raw, "false")) return "false"; + if (std.ascii.eqlIgnoreCase(raw, "plaintext-only")) return "plaintext-only"; + return "inherit"; +} + +pub fn setContentEditable(self: *HtmlElement, value: []const u8, frame: *Frame) !void { + const el = self.asElement(); + if (std.ascii.eqlIgnoreCase(value, "inherit")) { + return el.removeAttribute(.wrap("contenteditable"), frame); + } + inline for (.{ "true", "false", "plaintext-only" }) |keyword| { + if (std.ascii.eqlIgnoreCase(value, keyword)) { + return el.setAttributeSafe(.wrap("contenteditable"), .wrap(keyword), frame); + } + } + return error.SyntaxError; +} + pub fn getIsContentEditable(self: *HtmlElement) bool { var current: ?*Element = self.asElement(); while (current) |el| : (current = el.parentElement()) { @@ -1839,6 +1861,7 @@ pub const JsApi = struct { pub const showPopover = bridge.function(HtmlElement.showPopover, .{}); pub const hidePopover = bridge.function(HtmlElement.hidePopover, .{}); pub const togglePopover = bridge.function(HtmlElement.togglePopover, .{}); + pub const contentEditable = bridge.accessor(HtmlElement.getContentEditable, HtmlElement.setContentEditable, .{ .ce_reactions = true }); pub const isContentEditable = bridge.accessor(HtmlElement.getIsContentEditable, null, .{}); pub const lang = bridge.accessor(HtmlElement.getLang, HtmlElement.setLang, .{ .ce_reactions = true }); pub const nonce = bridge.accessor(HtmlElement.getNonce, HtmlElement.setNonce, .{ .ce_reactions = true });