mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-14 23:38:57 +00:00
Element.matches, Element.hasAttributes and DOMStringMap (Element.dataset)
This commit is contained in:
@@ -63,6 +63,9 @@ _attribute_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attribute),
|
||||
// the return of elements.attributes.
|
||||
_attribute_named_node_map_lookup: std.AutoHashMapUnmanaged(usize, *Element.Attribute.NamedNodeMap),
|
||||
|
||||
// element.dataset -> DOMStringMap
|
||||
_element_datasets: std.AutoHashMapUnmanaged(*Element, *Element.DOMStringMap),
|
||||
|
||||
_script_manager: ScriptManager,
|
||||
|
||||
_polyfill_loader: polyfill.Loader = .{},
|
||||
@@ -152,6 +155,7 @@ fn reset(self: *Page, comptime initializing: bool) !void {
|
||||
self._load_state = .parsing;
|
||||
self._attribute_lookup = .empty;
|
||||
self._attribute_named_node_map_lookup = .empty;
|
||||
self._element_datasets = .empty;
|
||||
self._event_manager = EventManager.init(self);
|
||||
|
||||
self._script_manager = ScriptManager.init(self);
|
||||
|
||||
@@ -707,7 +707,6 @@ const Script = struct {
|
||||
.cacheable = cacheable,
|
||||
});
|
||||
|
||||
|
||||
// Handle importmap special case here: the content is a JSON containing
|
||||
// imports.
|
||||
if (self.kind == .importmap) {
|
||||
|
||||
@@ -157,7 +157,7 @@ pub fn _getIndex(self: *Caller, comptime T: type, func: anytype, idx: u32, info:
|
||||
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
|
||||
@field(args, "1") = idx;
|
||||
const ret = @call(.auto, func, args);
|
||||
return self.handleIndexedReturn(T, F, ret, info, opts);
|
||||
return self.handleIndexedReturn(T, F, true, ret, info, opts);
|
||||
}
|
||||
|
||||
pub fn getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||
@@ -173,10 +173,49 @@ pub fn _getNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.N
|
||||
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
|
||||
@field(args, "1") = try self.nameToString(name);
|
||||
const ret = @call(.auto, func, args);
|
||||
return self.handleIndexedReturn(T, F, ret, info, opts);
|
||||
return self.handleIndexedReturn(T, F, true, ret, info, opts);
|
||||
}
|
||||
|
||||
fn handleIndexedReturn(self: *Caller, comptime T: type, comptime F: type, ret: anytype, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
pub fn setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, js_value: v8.Value, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||
return self._setNamedIndex(T, func, name, js_value, info, opts) catch |err| {
|
||||
self.handleError(T, @TypeOf(func), err, info, opts);
|
||||
return v8.Intercepted.No;
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _setNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, js_value: v8.Value, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
const F = @TypeOf(func);
|
||||
var args: ParameterTypes(F) = undefined;
|
||||
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
|
||||
@field(args, "1") = try self.nameToString(name);
|
||||
@field(args, "2") = try self.context.jsValueToZig(@TypeOf(@field(args, "2")), js_value);
|
||||
if (@typeInfo(F).@"fn".params.len == 4) {
|
||||
@field(args, "3") = self.context.page;
|
||||
}
|
||||
const ret = @call(.auto, func, args);
|
||||
return self.handleIndexedReturn(T, F, false, ret, info, opts);
|
||||
}
|
||||
|
||||
pub fn deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) u8 {
|
||||
return self._deleteNamedIndex(T, func, name, info, opts) catch |err| {
|
||||
self.handleError(T, @TypeOf(func), err, info, opts);
|
||||
return v8.Intercepted.No;
|
||||
};
|
||||
}
|
||||
|
||||
pub fn _deleteNamedIndex(self: *Caller, comptime T: type, func: anytype, name: v8.Name, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
const F = @TypeOf(func);
|
||||
var args: ParameterTypes(F) = undefined;
|
||||
@field(args, "0") = try Context.typeTaggedAnyOpaque(*T, info.getThis());
|
||||
@field(args, "1") = try self.nameToString(name);
|
||||
if (@typeInfo(F).@"fn".params.len == 3) {
|
||||
@field(args, "2") = self.context.page;
|
||||
}
|
||||
const ret = @call(.auto, func, args);
|
||||
return self.handleIndexedReturn(T, F, false, ret, info, opts);
|
||||
}
|
||||
|
||||
fn handleIndexedReturn(self: *Caller, comptime T: type, comptime F: type, comptime getter: bool, ret: anytype, info: v8.PropertyCallbackInfo, comptime opts: CallOpts) !u8 {
|
||||
// need to unwrap this error immediately for when opts.null_as_undefined == true
|
||||
// and we need to compare it to null;
|
||||
const non_error_ret = switch (@typeInfo(@TypeOf(ret))) {
|
||||
@@ -197,7 +236,9 @@ fn handleIndexedReturn(self: *Caller, comptime T: type, comptime F: type, ret: a
|
||||
else => ret,
|
||||
};
|
||||
|
||||
if (comptime getter) {
|
||||
info.getReturnValue().set(try self.context.zigValueToJs(non_error_ret, opts));
|
||||
}
|
||||
return v8.Intercepted.Yes;
|
||||
}
|
||||
|
||||
|
||||
@@ -253,13 +253,12 @@ pub fn attachClass(comptime JsApi: type, isolate: v8.Isolate, template: v8.Funct
|
||||
};
|
||||
template_proto.setIndexedProperty(configuration, null);
|
||||
},
|
||||
bridge.NamedIndexed => {
|
||||
const configuration = v8.NamedPropertyHandlerConfiguration{
|
||||
bridge.NamedIndexed => template.getInstanceTemplate().setNamedProperty(.{
|
||||
.getter = value.getter,
|
||||
.setter = value.setter,
|
||||
.deleter = value.deleter,
|
||||
.flags = v8.PropertyHandlerFlags.OnlyInterceptStrings | v8.PropertyHandlerFlags.NonMasking,
|
||||
};
|
||||
template_proto.setNamedProperty(configuration, null);
|
||||
},
|
||||
}, null),
|
||||
bridge.Iterator => {
|
||||
// Same as a function, but with a specific name
|
||||
const function_template = v8.FunctionTemplate.initCallback(isolate, value.func);
|
||||
@@ -326,7 +325,6 @@ fn generateConstructor(comptime JsApi: type, isolate: v8.Isolate) v8.FunctionTem
|
||||
|
||||
// if (has_js_call_as_function) {
|
||||
|
||||
|
||||
// if (@hasDecl(Struct, "htmldda") and Struct.htmldda) {
|
||||
// if (!has_js_call_as_function) {
|
||||
// @compileError(@typeName(Struct) ++ ": htmldda required jsCallAsFunction to be defined. This is a hard-coded requirement in V8, because mark_as_undetectable only exists for HTMLAllCollection which is also callable.");
|
||||
|
||||
@@ -45,8 +45,8 @@ pub fn Builder(comptime T: type) type {
|
||||
return Indexed.init(T, getter_func, opts);
|
||||
}
|
||||
|
||||
pub fn namedIndexed(comptime getter_func: anytype, comptime opts: NamedIndexed.Opts) NamedIndexed {
|
||||
return NamedIndexed.init(T, getter_func, opts);
|
||||
pub fn namedIndexed(comptime getter_func: anytype, setter_func: anytype, deleter_func: anytype, comptime opts: NamedIndexed.Opts) NamedIndexed {
|
||||
return NamedIndexed.init(T, getter_func, setter_func, deleter_func, opts);
|
||||
}
|
||||
|
||||
pub fn iterator(comptime func: anytype, comptime opts: Iterator.Opts) Iterator {
|
||||
@@ -221,14 +221,16 @@ pub const Indexed = struct {
|
||||
|
||||
pub const NamedIndexed = struct {
|
||||
getter: *const fn (c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8,
|
||||
setter: ?*const fn (c_name: ?*const v8.C_Name, c_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 = null,
|
||||
deleter: ?*const fn (c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 = null,
|
||||
|
||||
const Opts = struct {
|
||||
as_typed_array: bool = false,
|
||||
null_as_undefined: bool = false,
|
||||
};
|
||||
|
||||
fn init(comptime T: type, comptime getter: anytype, comptime opts: Opts) NamedIndexed {
|
||||
return .{ .getter = struct {
|
||||
fn init(comptime T: type, comptime getter: anytype, setter: anytype, deleter: anytype, comptime opts: Opts) NamedIndexed {
|
||||
const getter_fn = struct {
|
||||
fn wrap(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
||||
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
||||
var caller = Caller.init(info);
|
||||
@@ -238,7 +240,39 @@ pub const NamedIndexed = struct {
|
||||
.null_as_undefined = opts.null_as_undefined,
|
||||
});
|
||||
}
|
||||
}.wrap };
|
||||
}.wrap;
|
||||
|
||||
const setter_fn = if (@typeInfo(@TypeOf(setter)) == .null) null else struct {
|
||||
fn wrap(c_name: ?*const v8.C_Name, c_value: ?*const v8.C_Value, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
||||
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
||||
var caller = Caller.init(info);
|
||||
defer caller.deinit();
|
||||
|
||||
return caller.setNamedIndex(T, setter, .{ .handle = c_name.? }, .{ .handle = c_value.? }, info, .{
|
||||
.as_typed_array = opts.as_typed_array,
|
||||
.null_as_undefined = opts.null_as_undefined,
|
||||
});
|
||||
}
|
||||
}.wrap;
|
||||
|
||||
const deleter_fn = if (@typeInfo(@TypeOf(deleter)) == .null) null else struct {
|
||||
fn wrap(c_name: ?*const v8.C_Name, raw_info: ?*const v8.C_PropertyCallbackInfo) callconv(.c) u8 {
|
||||
const info = v8.PropertyCallbackInfo.initFromV8(raw_info);
|
||||
var caller = Caller.init(info);
|
||||
defer caller.deinit();
|
||||
|
||||
return caller.deleteNamedIndex(T, deleter, .{ .handle = c_name.? }, info, .{
|
||||
.as_typed_array = opts.as_typed_array,
|
||||
.null_as_undefined = opts.null_as_undefined,
|
||||
});
|
||||
}
|
||||
}.wrap;
|
||||
|
||||
return .{
|
||||
.getter = getter_fn,
|
||||
.setter = setter_fn,
|
||||
.deleter = deleter_fn,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -269,7 +303,6 @@ pub const Iterator = struct {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
pub const Callable = struct {
|
||||
func: *const fn (?*const v8.C_FunctionCallbackInfo) callconv(.c) void,
|
||||
|
||||
@@ -278,7 +311,7 @@ pub const Callable = struct {
|
||||
};
|
||||
|
||||
fn init(comptime T: type, comptime func: anytype, comptime opts: Opts) Callable {
|
||||
return .{.func = struct {
|
||||
return .{ .func = struct {
|
||||
fn wrap(raw_info: ?*const v8.C_FunctionCallbackInfo) callconv(.c) void {
|
||||
const info = v8.FunctionCallbackInfo.initFromV8(raw_info);
|
||||
var caller = Caller.init(info);
|
||||
@@ -286,8 +319,8 @@ pub const Callable = struct {
|
||||
caller.method(T, func, info, .{
|
||||
.null_as_undefined = opts.null_as_undefined,
|
||||
});
|
||||
}}.wrap
|
||||
};
|
||||
}
|
||||
}.wrap };
|
||||
}
|
||||
};
|
||||
|
||||
@@ -457,6 +490,7 @@ pub const JsApis = flattenTypes(&.{
|
||||
@import("../webapi/DOMNodeIterator.zig"),
|
||||
@import("../webapi/NodeFilter.zig"),
|
||||
@import("../webapi/Element.zig"),
|
||||
@import("../webapi/element/DOMStringMap.zig"),
|
||||
@import("../webapi/element/Attribute.zig"),
|
||||
@import("../webapi/element/Html.zig"),
|
||||
@import("../webapi/element/html/IFrame.zig"),
|
||||
|
||||
@@ -83,3 +83,22 @@
|
||||
|
||||
assertAttributes([{name: 'id', value: 'attr1'}, {name: 'class', value: 'sHow'}]);
|
||||
</script>
|
||||
|
||||
<script id=hasAttribute>
|
||||
{
|
||||
const el1 = $('#attr1');
|
||||
|
||||
testing.expectEqual(true, el1.hasAttribute('id'));
|
||||
testing.expectEqual(true, el1.hasAttribute('ID'));
|
||||
testing.expectEqual(true, el1.hasAttribute('class'));
|
||||
testing.expectEqual(true, el1.hasAttribute('CLASS'));
|
||||
testing.expectEqual(false, el1.hasAttribute('other'));
|
||||
testing.expectEqual(false, el1.hasAttribute('nope'));
|
||||
|
||||
el1.setAttribute('data-test', 'value');
|
||||
testing.expectEqual(true, el1.hasAttribute('data-test'));
|
||||
|
||||
el1.removeAttribute('data-test');
|
||||
testing.expectEqual(false, el1.hasAttribute('data-test'));
|
||||
}
|
||||
</script>
|
||||
|
||||
150
src/browser/tests/element/dataset.html
Normal file
150
src/browser/tests/element/dataset.html
Normal file
@@ -0,0 +1,150 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<div id="test" data-foo="bar" data-hello-world="test"></div>
|
||||
<div id="test-write"></div>
|
||||
<div id="test-bracket"></div>
|
||||
|
||||
<script id=basic>
|
||||
{
|
||||
const el = document.getElementById('test');
|
||||
testing.expectEqual('object', typeof el.dataset);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=readExistingAttributes>
|
||||
{
|
||||
const el = document.getElementById('test');
|
||||
testing.expectEqual('bar', el.dataset.foo);
|
||||
testing.expectEqual('test', el.dataset.helloWorld);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=camelCaseConversion>
|
||||
{
|
||||
const el = document.getElementById('test');
|
||||
|
||||
// Reading kebab-case as camelCase
|
||||
testing.expectEqual('test', el.dataset.helloWorld);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=writeNewAttribute>
|
||||
{
|
||||
const el = document.getElementById('test-write');
|
||||
el.dataset.newAttr = 'value';
|
||||
|
||||
testing.expectEqual('value', el.dataset.newAttr);
|
||||
testing.expectEqual('value', el.getAttribute('data-new-attr'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=writeExistingAttribute>
|
||||
{
|
||||
const el = document.getElementById('test-write');
|
||||
el.setAttribute('data-foo', 'original');
|
||||
el.dataset.foo = 'updated';
|
||||
|
||||
testing.expectEqual('updated', el.dataset.foo);
|
||||
testing.expectEqual('updated', el.getAttribute('data-foo'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=writeCamelCaseConversion>
|
||||
{
|
||||
const el = document.getElementById('test-write');
|
||||
|
||||
// Writing camelCase creates kebab-case attribute
|
||||
el.dataset.fooBarBaz = 'qux';
|
||||
testing.expectEqual('qux', el.getAttribute('data-foo-bar-baz'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=undefinedForNonExistent>
|
||||
{
|
||||
const el = document.getElementById('test');
|
||||
testing.expectEqual(undefined, el.dataset.nonExistent);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=bracketNotation>
|
||||
{
|
||||
const el = document.getElementById('test-bracket');
|
||||
el.setAttribute('data-foo', 'bar');
|
||||
el.setAttribute('data-hello-world', 'test');
|
||||
|
||||
// Bracket notation should work the same as dot notation
|
||||
testing.expectEqual('bar', el.dataset['foo']);
|
||||
testing.expectEqual('test', el.dataset['helloWorld']);
|
||||
|
||||
// Non-existent should return undefined
|
||||
testing.expectEqual(undefined, el.dataset['nonExistent']);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=edgeCases>
|
||||
{
|
||||
const el = document.createElement('div');
|
||||
el.setAttribute('data-x', 'single-letter');
|
||||
el.setAttribute('data-foo-bar-baz', 'multiple-dashes');
|
||||
|
||||
// Single letter key
|
||||
testing.expectEqual('single-letter', el.dataset['x']);
|
||||
testing.expectEqual('single-letter', el.dataset.x);
|
||||
|
||||
// Multiple dashes
|
||||
testing.expectEqual('multiple-dashes', el.dataset['fooBarBaz']);
|
||||
testing.expectEqual('multiple-dashes', el.dataset.fooBarBaz);
|
||||
|
||||
// Empty string key (data- attribute) - should be accessible as empty string
|
||||
el.setAttribute('data-', 'empty');
|
||||
testing.expectEqual('empty', el.dataset['']);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=identityCheck>
|
||||
{
|
||||
const el = document.getElementById('test');
|
||||
const ds1 = el.dataset;
|
||||
const ds2 = el.dataset;
|
||||
|
||||
// Should return the same object instance
|
||||
testing.expectEqual(true, ds1 === ds2);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=deleteProperty>
|
||||
{
|
||||
const el = document.createElement('div');
|
||||
el.setAttribute('data-foo', 'bar');
|
||||
el.setAttribute('data-hello-world', 'test');
|
||||
|
||||
testing.expectEqual('bar', el.dataset.foo);
|
||||
testing.expectEqual('test', el.dataset.helloWorld);
|
||||
|
||||
// Delete using dot notation
|
||||
delete el.dataset.foo;
|
||||
testing.expectEqual(undefined, el.dataset.foo);
|
||||
testing.expectEqual(null, el.getAttribute('data-foo'));
|
||||
|
||||
// Delete using bracket notation
|
||||
delete el.dataset['helloWorld'];
|
||||
testing.expectEqual(undefined, el.dataset.helloWorld);
|
||||
testing.expectEqual(null, el.getAttribute('data-hello-world'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=independentInstances>
|
||||
{
|
||||
const el1 = document.getElementById('test');
|
||||
const el2 = document.createElement('div');
|
||||
|
||||
el1.dataset.test1 = 'value1';
|
||||
el2.dataset.test2 = 'value2';
|
||||
|
||||
testing.expectEqual('value1', el1.dataset.test1);
|
||||
testing.expectEqual(undefined, el1.dataset.test2);
|
||||
testing.expectEqual(undefined, el2.dataset.test1);
|
||||
testing.expectEqual('value2', el2.dataset.test2);
|
||||
}
|
||||
</script>
|
||||
76
src/browser/tests/element/matches.html
Normal file
76
src/browser/tests/element/matches.html
Normal file
@@ -0,0 +1,76 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<div id="test-container" class="container main">
|
||||
<p class="text highlight">Paragraph 1</p>
|
||||
<div class="nested">
|
||||
<p class="text">Paragraph 2</p>
|
||||
<span id="special" class="wrapper">
|
||||
<p class="deep">Paragraph 3</p>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script id=basicMatches>
|
||||
{
|
||||
const container = $('#test-container');
|
||||
|
||||
testing.expectEqual(true, container.matches('#test-container'));
|
||||
testing.expectEqual(false, container.matches('#other'));
|
||||
|
||||
testing.expectEqual(true, container.matches('.container'));
|
||||
testing.expectEqual(true, container.matches('.main'));
|
||||
testing.expectEqual(false, container.matches('.nested'));
|
||||
|
||||
testing.expectEqual(true, container.matches('div'));
|
||||
testing.expectEqual(false, container.matches('p'));
|
||||
|
||||
testing.expectEqual(true, container.matches('div.container'));
|
||||
testing.expectEqual(true, container.matches('div#test-container'));
|
||||
testing.expectEqual(true, container.matches('.container.main'));
|
||||
testing.expectEqual(false, container.matches('div.nested'));
|
||||
|
||||
testing.expectEqual(true, container.matches('*'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=childMatches>
|
||||
{
|
||||
const paragraph = $('#test-container > p');
|
||||
|
||||
testing.expectEqual(true, paragraph.matches('p'));
|
||||
testing.expectEqual(true, paragraph.matches('.text'));
|
||||
testing.expectEqual(true, paragraph.matches('.highlight'));
|
||||
testing.expectEqual(true, paragraph.matches('p.text.highlight'));
|
||||
|
||||
testing.expectEqual(false, paragraph.matches('#test-container'));
|
||||
testing.expectEqual(false, paragraph.matches('div'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=specialSpan>
|
||||
{
|
||||
const span = $('#special');
|
||||
|
||||
testing.expectEqual(true, span.matches('#special'));
|
||||
testing.expectEqual(true, span.matches('span'));
|
||||
testing.expectEqual(true, span.matches('.wrapper'));
|
||||
testing.expectEqual(true, span.matches('span.wrapper'));
|
||||
testing.expectEqual(true, span.matches('span#special.wrapper'));
|
||||
testing.expectEqual(false, span.matches('#other'));
|
||||
testing.expectEqual(false, span.matches('div'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=errorHandling>
|
||||
{
|
||||
const container = $('#test-container');
|
||||
|
||||
testing.expectError("Syntax Error", () => container.matches(''));
|
||||
testing.withError((err) => {
|
||||
testing.expectEqual(12, err.code);
|
||||
testing.expectEqual("SyntaxError", err.name);
|
||||
testing.expectEqual("Syntax Error", err.message);
|
||||
}, () => container.matches(''));
|
||||
}
|
||||
</script>
|
||||
@@ -195,11 +195,11 @@ pub const JsApi = struct {
|
||||
pub const querySelectorAll = bridge.function(Document.querySelectorAll, .{ .dom_exception = true });
|
||||
pub const getElementsByTagName = bridge.function(Document.getElementsByTagName, .{});
|
||||
pub const getElementsByClassName = bridge.function(Document.getElementsByClassName, .{});
|
||||
pub const defaultView = bridge.accessor(struct{
|
||||
pub const defaultView = bridge.accessor(struct {
|
||||
fn defaultView(_: *const Document, page: *Page) *@import("Window.zig") {
|
||||
return page.window;
|
||||
}
|
||||
}.defaultView, null, .{.cache = "defaultView"});
|
||||
}.defaultView, null, .{ .cache = "defaultView" });
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
|
||||
@@ -12,6 +12,7 @@ const collections = @import("collections.zig");
|
||||
const Selector = @import("selector/Selector.zig");
|
||||
pub const Attribute = @import("element/Attribute.zig");
|
||||
const CSSStyleProperties = @import("css/CSSStyleProperties.zig");
|
||||
pub const DOMStringMap = @import("element/DOMStringMap.zig");
|
||||
|
||||
pub const Svg = @import("element/Svg.zig");
|
||||
pub const Html = @import("element/Html.zig");
|
||||
@@ -247,6 +248,12 @@ pub fn getAttribute(self: *const Element, name: []const u8, page: *Page) !?[]con
|
||||
return attributes.get(name, page);
|
||||
}
|
||||
|
||||
pub fn hasAttribute(self: *const Element, name: []const u8, page: *Page) !bool {
|
||||
const attributes = self._attributes orelse return false;
|
||||
const value = try attributes.get(name, page);
|
||||
return value != null;
|
||||
}
|
||||
|
||||
pub fn getAttributeNode(self: *Element, name: []const u8, page: *Page) !?*Attribute {
|
||||
const attributes = self._attributes orelse return null;
|
||||
return attributes.getAttribute(name, self, page);
|
||||
@@ -342,6 +349,16 @@ pub fn getClassList(self: *Element, page: *Page) !*collections.DOMTokenList {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getDataset(self: *Element, page: *Page) !*DOMStringMap {
|
||||
const gop = try page._element_datasets.getOrPut(page.arena, self);
|
||||
if (!gop.found_existing) {
|
||||
gop.value_ptr.* = try page._factory.create(DOMStringMap{
|
||||
._element = self,
|
||||
});
|
||||
}
|
||||
return gop.value_ptr.*;
|
||||
}
|
||||
|
||||
pub fn replaceChildren(self: *Element, nodes: []const Node.NodeOrText, page: *Page) !void {
|
||||
page.domChanged();
|
||||
var parent = self.asNode();
|
||||
@@ -438,6 +455,10 @@ pub fn getChildElementCount(self: *Element) usize {
|
||||
return count;
|
||||
}
|
||||
|
||||
pub fn matches(self: *Element, selector: []const u8, page: *Page) !bool {
|
||||
return Selector.matches(self, selector, page);
|
||||
}
|
||||
|
||||
pub fn querySelector(self: *Element, selector: []const u8, page: *Page) !?*Element {
|
||||
return Selector.querySelector(self.asNode(), selector, page);
|
||||
}
|
||||
@@ -658,8 +679,10 @@ pub const JsApi = struct {
|
||||
pub const id = bridge.accessor(Element.getId, Element.setId, .{});
|
||||
pub const className = bridge.accessor(Element.getClassName, Element.setClassName, .{});
|
||||
pub const classList = bridge.accessor(Element.getClassList, null, .{});
|
||||
pub const dataset = bridge.accessor(Element.getDataset, null, .{});
|
||||
pub const style = bridge.accessor(Element.getStyle, null, .{});
|
||||
pub const attributes = bridge.accessor(Element.getAttributeNamedNodeMap, null, .{});
|
||||
pub const hasAttribute = bridge.function(Element.hasAttribute, .{});
|
||||
pub const getAttribute = bridge.function(Element.getAttribute, .{});
|
||||
pub const getAttributeNode = bridge.function(Element.getAttributeNode, .{});
|
||||
pub const setAttribute = bridge.function(Element.setAttribute, .{});
|
||||
@@ -676,6 +699,7 @@ pub const JsApi = struct {
|
||||
pub const nextElementSibling = bridge.accessor(Element.nextElementSibling, null, .{});
|
||||
pub const previousElementSibling = bridge.accessor(Element.previousElementSibling, null, .{});
|
||||
pub const childElementCount = bridge.accessor(Element.getChildElementCount, null, .{});
|
||||
pub const matches = bridge.function(Element.matches, .{ .dom_exception = true });
|
||||
pub const querySelector = bridge.function(Element.querySelector, .{ .dom_exception = true });
|
||||
pub const querySelectorAll = bridge.function(Element.querySelectorAll, .{ .dom_exception = true });
|
||||
pub const getElementsByTagName = bridge.function(Element.getElementsByTagName, .{});
|
||||
|
||||
@@ -614,9 +614,7 @@ pub const JsApi = struct {
|
||||
|
||||
pub const textContent = bridge.accessor(_textContext, Node.setTextContent, .{});
|
||||
fn _textContext(self: *Node, page: *const Page) !?[]const u8 {
|
||||
// can't call node.getTextContent directly, because
|
||||
// 1 - document should return null, not empty
|
||||
// 2 - cdata and attributes can return value directly, avoiding the copy
|
||||
// cdata and attributes can return value directly, avoiding the copy
|
||||
switch (self._type) {
|
||||
.element => |el| {
|
||||
var buf = std.Io.Writer.Allocating.init(page.call_arena);
|
||||
|
||||
@@ -152,7 +152,7 @@ pub const JsApi = struct {
|
||||
|
||||
pub const length = bridge.accessor(HTMLAllCollection.length, null, .{});
|
||||
pub const @"[int]" = bridge.indexed(HTMLAllCollection.getAtIndex, .{ .null_as_undefined = true });
|
||||
pub const @"[str]" = bridge.namedIndexed(HTMLAllCollection.getByName, .{ .null_as_undefined = true });
|
||||
pub const @"[str]" = bridge.namedIndexed(HTMLAllCollection.getByName, null, null, .{ .null_as_undefined = true });
|
||||
|
||||
pub const item = bridge.function(_item, .{});
|
||||
fn _item(self: *HTMLAllCollection, index: i32, page: *Page) ?*Element {
|
||||
|
||||
@@ -83,7 +83,7 @@ pub const JsApi = struct {
|
||||
|
||||
pub const length = bridge.accessor(HTMLCollection.length, null, .{});
|
||||
pub const @"[int]" = bridge.indexed(HTMLCollection.getAtIndex, .{ .null_as_undefined = true });
|
||||
pub const @"[str]" = bridge.namedIndexed(HTMLCollection.getByName, .{ .null_as_undefined = true });
|
||||
pub const @"[str]" = bridge.namedIndexed(HTMLCollection.getByName, null, null, .{ .null_as_undefined = true });
|
||||
|
||||
pub const item = bridge.function(_item, .{});
|
||||
fn _item(self: *HTMLCollection, index: i32, page: *Page) ?*Element {
|
||||
|
||||
@@ -120,7 +120,7 @@ pub const JsApi = struct {
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const @"[]" = bridge.namedIndexed(_getPropertyIndexed, .{});
|
||||
pub const @"[]" = bridge.namedIndexed(_getPropertyIndexed, null, null, .{});
|
||||
|
||||
const method_names = std.StaticStringMap(void).initComptime(.{
|
||||
.{ "getPropertyValue", {} },
|
||||
|
||||
@@ -386,7 +386,7 @@ pub const NamedNodeMap = struct {
|
||||
|
||||
pub const length = bridge.accessor(NamedNodeMap.length, null, .{});
|
||||
pub const @"[int]" = bridge.indexed(NamedNodeMap.getAtIndex, .{ .null_as_undefined = true });
|
||||
pub const @"[str]" = bridge.namedIndexed(NamedNodeMap.getByName, .{ .null_as_undefined = true });
|
||||
pub const @"[str]" = bridge.namedIndexed(NamedNodeMap.getByName, null, null, .{ .null_as_undefined = true });
|
||||
pub const getNamedItem = bridge.function(NamedNodeMap.getByName, .{});
|
||||
pub const item = bridge.function(_item, .{});
|
||||
fn _item(self: *const NamedNodeMap, index: i32, page: *Page) !?*Attribute {
|
||||
|
||||
87
src/browser/webapi/element/DOMStringMap.zig
Normal file
87
src/browser/webapi/element/DOMStringMap.zig
Normal file
@@ -0,0 +1,87 @@
|
||||
const std = @import("std");
|
||||
const js = @import("../../js/js.zig");
|
||||
|
||||
const Element = @import("../Element.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const DOMStringMap = @This();
|
||||
|
||||
_element: *Element,
|
||||
|
||||
fn _getProperty(self: *DOMStringMap, name: []const u8, page: *Page) !?[]const u8 {
|
||||
const attr_name = try camelToKebab(page.call_arena, name);
|
||||
return try self._element.getAttribute(attr_name, page);
|
||||
}
|
||||
|
||||
fn _setProperty(self: *DOMStringMap, name: []const u8, value: []const u8, page: *Page) !void {
|
||||
const attr_name = try camelToKebab(page.call_arena, name);
|
||||
return self._element.setAttributeSafe(attr_name, value, page);
|
||||
}
|
||||
|
||||
fn _deleteProperty(self: *DOMStringMap, name: []const u8, page: *Page) !void {
|
||||
const attr_name = try camelToKebab(page.call_arena, name);
|
||||
try self._element.removeAttribute(attr_name, page);
|
||||
}
|
||||
|
||||
// fooBar -> foo-bar
|
||||
fn camelToKebab(arena: Allocator, camel: []const u8) ![]const u8 {
|
||||
var result: std.ArrayList(u8) = .empty;
|
||||
try result.ensureTotalCapacity(arena, 5 + camel.len * 2);
|
||||
result.appendSliceAssumeCapacity("data-");
|
||||
|
||||
for (camel, 0..) |c, i| {
|
||||
if (std.ascii.isUpper(c)) {
|
||||
if (i > 0) {
|
||||
result.appendAssumeCapacity('-');
|
||||
}
|
||||
result.appendAssumeCapacity(std.ascii.toLower(c));
|
||||
} else {
|
||||
result.appendAssumeCapacity(c);
|
||||
}
|
||||
}
|
||||
|
||||
return result.items;
|
||||
}
|
||||
|
||||
// data-foo-bar -> fooBar
|
||||
fn kebabToCamel(arena: Allocator, kebab: []const u8) !?[]const u8 {
|
||||
if (!std.mem.startsWith(u8, kebab, "data-")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const data_part = kebab[5..]; // Skip "data-"
|
||||
if (data_part.len == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var result: std.ArrayList(u8) = .empty;
|
||||
try result.ensureTotalCapacity(arena, data_part.len);
|
||||
|
||||
var capitalize_next = false;
|
||||
for (data_part) |c| {
|
||||
if (c == '-') {
|
||||
capitalize_next = true;
|
||||
} else if (capitalize_next) {
|
||||
result.appendAssumeCapacity(std.ascii.toUpper(c));
|
||||
capitalize_next = false;
|
||||
} else {
|
||||
result.appendAssumeCapacity(c);
|
||||
}
|
||||
}
|
||||
|
||||
return result.items;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(DOMStringMap);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "DOMStringMap";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const @"[]" = bridge.namedIndexed(_getProperty, _setProperty, _deleteProperty, .{.null_as_undefined = true});
|
||||
};
|
||||
@@ -33,7 +33,7 @@ pub const Build = struct {
|
||||
const el = node.as(Element);
|
||||
const on_load = el.getAttributeSafe("onload") orelse return;
|
||||
page.window._on_load = page.js.stringToFunction(on_load) catch |err| blk: {
|
||||
log.err(.js, "body.onload", .{.err = err, .str = on_load});
|
||||
log.err(.js, "body.onload", .{ .err = err, .str = on_load });
|
||||
break :blk null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -80,14 +80,14 @@ pub const Build = struct {
|
||||
|
||||
if (element.getAttributeSafe("onload")) |on_load| {
|
||||
self._on_load = page.js.stringToFunction(on_load) catch |err| blk: {
|
||||
log.err(.js, "script.onload", .{.err = err, .str = on_load});
|
||||
log.err(.js, "script.onload", .{ .err = err, .str = on_load });
|
||||
break :blk null;
|
||||
};
|
||||
}
|
||||
|
||||
if (element.getAttributeSafe("onerror")) |on_error| {
|
||||
self._on_error = page.js.stringToFunction(on_error) catch |err| blk: {
|
||||
log.err(.js, "script.onerror", .{.err = err, .str = on_error});
|
||||
log.err(.js, "script.onerror", .{ .err = err, .str = on_error });
|
||||
break :blk null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -77,7 +77,6 @@ pub fn parse(arena: Allocator, input: []const u8, page: *Page) ParseError!Select
|
||||
var segments: std.ArrayList(Segment) = .empty;
|
||||
var current_compound: std.ArrayList(Part) = .empty;
|
||||
|
||||
|
||||
// Parse the first compound (no combinator before it)
|
||||
while (parser.skipSpaces()) {
|
||||
if (parser.peek() == 0) break;
|
||||
|
||||
Reference in New Issue
Block a user