Merge pull request #2310 from navidemad/fix-a22-iscontenteditable

dom: implement HTMLElement.isContentEditable IDL attribute
This commit is contained in:
Karl Seguin
2026-04-30 16:09:56 +08:00
committed by GitHub
2 changed files with 89 additions and 0 deletions

View File

@@ -0,0 +1,60 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<!--
Lightpanda has no caret/keyboard editing pipeline, so isContentEditable
always returns false regardless of the element's effective contenteditable
state. The spec walk still runs internally to log .not_implemented when the
spec would have said true; only the return value is asserted here. See
Html.zig:getIsContentEditable and PR #2310 for context.
-->
<div id="own-true" contenteditable="true">own true</div>
<div id="own-empty" contenteditable="">own empty</div>
<div id="own-false" contenteditable="false">own false</div>
<div id="own-plaintext" contenteditable="plaintext-only">own plaintext-only</div>
<div id="ancestor-true" contenteditable="true">
<span id="child-of-true">child</span>
</div>
<div id="ancestor-false" contenteditable="false">
<div contenteditable="true">
<span id="grandchild-rehosted">re-hosted</span>
</div>
</div>
<div id="plain"><span id="child-of-plain">no editing context</span></div>
<script id="always-false">
{
testing.expectEqual(false, document.getElementById('own-true').isContentEditable);
testing.expectEqual(false, document.getElementById('own-empty').isContentEditable);
testing.expectEqual(false, document.getElementById('own-false').isContentEditable);
testing.expectEqual(false, document.getElementById('own-plaintext').isContentEditable);
testing.expectEqual(false, document.getElementById('ancestor-true').isContentEditable);
testing.expectEqual(false, document.getElementById('child-of-true').isContentEditable);
testing.expectEqual(false, document.getElementById('ancestor-false').isContentEditable);
testing.expectEqual(false, document.getElementById('grandchild-rehosted').isContentEditable);
testing.expectEqual(false, document.getElementById('plain').isContentEditable);
testing.expectEqual(false, document.getElementById('child-of-plain').isContentEditable);
testing.expectEqual(false, document.body.isContentEditable);
testing.expectEqual(false, document.documentElement.isContentEditable);
}
</script>
<script id="dynamic-attribute">
{
const el = document.createElement('div');
testing.expectEqual(false, el.isContentEditable);
el.setAttribute('contenteditable', 'true');
testing.expectEqual(false, el.isContentEditable);
el.setAttribute('contenteditable', 'false');
testing.expectEqual(false, el.isContentEditable);
el.removeAttribute('contenteditable');
testing.expectEqual(false, el.isContentEditable);
}
</script>

View File

@@ -382,6 +382,31 @@ pub fn setTitle(self: *HtmlElement, value: []const u8, frame: *Frame) !void {
try self.asElement().setAttributeSafe(comptime .wrap("title"), .wrap(value), frame);
}
// HTML §7.7.5.2 specifies the IDL attribute as true iff the element's effective
// content editable state is "true" or "plaintext-only". Lightpanda has no
// caret/keyboard editing pipeline, so a true answer cannot be honored
// end-to-end — downstream CDP tools (notably Puppeteer's dispatchKeyEvent
// path) would route into an input pipeline that silently no-ops. Always
// return false, and log .not_implemented when the spec would have said true
// so usage surfaces in telemetry rather than silently depending on an
// unsupported value. Spec walk per HTML §7.7.5.2 still applies — the nearest
// ancestor with `contenteditable` wins; "false" disables. See PR #2310 for
// the routing-vs-fail-loud discussion.
//
// "contenteditable" is 15 bytes — past the comptime SSO limit — so the
// String wrap runs at runtime, mirroring the pattern in interactive.zig.
pub fn getIsContentEditable(self: *HtmlElement) bool {
var current: ?*Element = self.asElement();
while (current) |el| : (current = el.parentElement()) {
const raw = el.getAttributeSafe(.wrap("contenteditable")) orelse continue;
if (!std.ascii.eqlIgnoreCase(raw, "false")) {
log.info(.not_implemented, "IsContentEditable", .{});
}
break;
}
return false;
}
pub fn getAttributeFunction(
self: *HtmlElement,
listener_type: GlobalEventHandler,
@@ -1220,6 +1245,7 @@ pub const JsApi = struct {
pub const dir = bridge.accessor(HtmlElement.getDir, HtmlElement.setDir, .{});
pub const hidden = bridge.accessor(HtmlElement.getHidden, HtmlElement.setHidden, .{});
pub const isContentEditable = bridge.accessor(HtmlElement.getIsContentEditable, null, .{});
pub const lang = bridge.accessor(HtmlElement.getLang, HtmlElement.setLang, .{});
pub const tabIndex = bridge.accessor(HtmlElement.getTabIndex, HtmlElement.setTabIndex, .{});
pub const title = bridge.accessor(HtmlElement.getTitle, HtmlElement.setTitle, .{});
@@ -1357,3 +1383,6 @@ test "WebApi: HTML.event_listeners" {
test "WebApi: HTMLElement.props" {
try testing.htmlRunner("element/html/htmlelement-props.html", .{});
}
test "WebApi: HTMLElement.contenteditable" {
try testing.htmlRunner("element/html/contenteditable.html", .{});
}