mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 09:35:59 -04:00
webapi: add DataTransfer, DataTransferItem, DataTransferItemList
Implements the constructible DataTransfer interface and its item views over a single drag-data-store: string- and file-kind items, with .files (FileList) and .types derived from the file items so items.add(file) is visible immediately. File refs are acquired on add and released on remove/clear; the backing FileList is frame-tracked and the DataTransfer arena is refcounted so the GC finalizer reclaims it, leaving no leaks. Refs #2043
This commit is contained in:
@@ -980,6 +980,7 @@ pub const PageJsApis = flattenTypes(&.{
|
||||
@import("../webapi/File.zig"),
|
||||
@import("../webapi/FileList.zig"),
|
||||
@import("../webapi/FileReader.zig"),
|
||||
@import("../webapi/DataTransfer.zig"),
|
||||
@import("../webapi/Screen.zig"),
|
||||
@import("../webapi/VisualViewport.zig"),
|
||||
@import("../webapi/PerformanceObserver.zig"),
|
||||
|
||||
128
src/browser/tests/data_transfer.html
Normal file
128
src/browser/tests/data_transfer.html
Normal file
@@ -0,0 +1,128 @@
|
||||
<!DOCTYPE html>
|
||||
<head>
|
||||
<title>Test DataTransfer Web API</title>
|
||||
<script src="./testing.js"></script>
|
||||
</head>
|
||||
|
||||
<script id=construction>
|
||||
{
|
||||
const dt = new DataTransfer();
|
||||
testing.expectEqual(true, dt instanceof DataTransfer);
|
||||
testing.expectEqual(0, dt.items.length);
|
||||
testing.expectEqual(0, dt.files.length);
|
||||
testing.expectEqual("none", dt.dropEffect);
|
||||
testing.expectEqual("uninitialized", dt.effectAllowed);
|
||||
testing.expectEqual(true, dt.items instanceof DataTransferItemList);
|
||||
testing.expectEqual(true, dt.files instanceof FileList);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=effects>
|
||||
{
|
||||
const dt = new DataTransfer();
|
||||
dt.dropEffect = "copy";
|
||||
testing.expectEqual("copy", dt.dropEffect);
|
||||
dt.dropEffect = "bogus"; // invalid ignored
|
||||
testing.expectEqual("copy", dt.dropEffect);
|
||||
dt.effectAllowed = "copyMove";
|
||||
testing.expectEqual("copyMove", dt.effectAllowed);
|
||||
dt.effectAllowed = "nope"; // invalid ignored
|
||||
testing.expectEqual("copyMove", dt.effectAllowed);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=string_data>
|
||||
{
|
||||
const dt = new DataTransfer();
|
||||
dt.setData("text/plain", "hello");
|
||||
testing.expectEqual("hello", dt.getData("text/plain"));
|
||||
// "text" normalizes to "text/plain"
|
||||
testing.expectEqual("hello", dt.getData("text"));
|
||||
// setData via the "text" alias overwrites the same item
|
||||
dt.setData("text", "world");
|
||||
testing.expectEqual("world", dt.getData("text/plain"));
|
||||
testing.expectEqual(1, dt.items.length);
|
||||
// "url" normalizes to "text/uri-list"
|
||||
dt.setData("url", "https://example.com");
|
||||
testing.expectEqual("https://example.com", dt.getData("text/uri-list"));
|
||||
testing.expectEqual(true, dt.types.indexOf("text/plain") !== -1);
|
||||
testing.expectEqual(true, dt.types.indexOf("text/uri-list") !== -1);
|
||||
// getData miss returns ""
|
||||
testing.expectEqual("", dt.getData("application/json"));
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=items_add_file>
|
||||
{
|
||||
const dt = new DataTransfer();
|
||||
const f = new File(["abc"], "a.txt", { type: "text/plain" });
|
||||
const item = dt.items.add(f);
|
||||
testing.expectEqual("file", item.kind);
|
||||
testing.expectEqual("text/plain", item.type);
|
||||
// file is reflected in .files immediately
|
||||
testing.expectEqual(1, dt.files.length);
|
||||
testing.expectEqual("a.txt", dt.files[0].name);
|
||||
testing.expectEqual(true, dt.files[0] instanceof File);
|
||||
// "Files" appears in types when a file item exists
|
||||
testing.expectEqual(true, dt.types.indexOf("Files") !== -1);
|
||||
testing.expectEqual(f, item.getAsFile());
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=items_add_string>
|
||||
{
|
||||
const dt = new DataTransfer();
|
||||
const item = dt.items.add("payload", "text/custom");
|
||||
testing.expectEqual("string", item.kind);
|
||||
testing.expectEqual("text/custom", item.type);
|
||||
testing.expectEqual("payload", dt.getData("text/custom"));
|
||||
testing.expectEqual(null, item.getAsFile());
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=get_as_string>
|
||||
{
|
||||
const dt = new DataTransfer();
|
||||
dt.setData("text/plain", "world");
|
||||
let got = null;
|
||||
dt.items[0].getAsString(function (s) { got = s; });
|
||||
testing.expectEqual("world", got);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=remove_and_clear>
|
||||
{
|
||||
const dt = new DataTransfer();
|
||||
dt.setData("text/plain", "s");
|
||||
dt.items.add(new File(["x"], "f.txt", { type: "text/plain" }));
|
||||
testing.expectEqual(2, dt.items.length);
|
||||
testing.expectEqual(1, dt.files.length);
|
||||
// remove the string item (index 0)
|
||||
dt.items.remove(0);
|
||||
testing.expectEqual(1, dt.items.length);
|
||||
testing.expectEqual("", dt.getData("text/plain"));
|
||||
testing.expectEqual(1, dt.files.length);
|
||||
// clearData() drops string items, keeps files
|
||||
dt.setData("text/plain", "again");
|
||||
dt.clearData();
|
||||
testing.expectEqual("", dt.getData("text/plain"));
|
||||
testing.expectEqual(1, dt.files.length);
|
||||
// items.clear() drops everything
|
||||
dt.items.clear();
|
||||
testing.expectEqual(0, dt.items.length);
|
||||
testing.expectEqual(0, dt.files.length);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=iterate_items>
|
||||
{
|
||||
const dt = new DataTransfer();
|
||||
dt.items.add("a", "text/a");
|
||||
dt.items.add("b", "text/b");
|
||||
const kinds = [];
|
||||
for (const it of dt.items) {
|
||||
kinds.push(it.type);
|
||||
}
|
||||
testing.expectEqual("text/a,text/b", kinds.join(","));
|
||||
}
|
||||
</script>
|
||||
291
src/browser/webapi/DataTransfer.zig
Normal file
291
src/browser/webapi/DataTransfer.zig
Normal file
@@ -0,0 +1,291 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const Frame = @import("../Frame.zig");
|
||||
const Page = @import("../Page.zig");
|
||||
|
||||
const File = @import("File.zig");
|
||||
const FileList = @import("FileList.zig");
|
||||
const DataTransferItem = @import("DataTransferItem.zig");
|
||||
const DataTransferItemList = @import("DataTransferItemList.zig");
|
||||
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/dnd.html#the-datatransfer-interface
|
||||
//
|
||||
// The canonical drag-data-store: one ordered list of items (string- or
|
||||
// file-kind). `.items` is a live DataTransferItemList view; `.files` is a
|
||||
// FileList rebuilt from the file-kind items so the two stay in sync. Per v1
|
||||
// scope the store is always read/write (no event-phase mode gating).
|
||||
const DataTransfer = @This();
|
||||
|
||||
pub fn registerTypes() []const type {
|
||||
return &.{
|
||||
DataTransfer,
|
||||
DataTransferItem,
|
||||
DataTransferItemList,
|
||||
DataTransferItemList.Iterator,
|
||||
};
|
||||
}
|
||||
|
||||
_arena: Allocator,
|
||||
// Refcounted so the GC weak-finalizer (or page teardown) releases the pooled
|
||||
// arena exactly once; mirrors Blob's lifecycle.
|
||||
_rc: lp.RC(u32) = .{},
|
||||
_items: std.ArrayList(*DataTransferItem) = .{},
|
||||
_item_list: *DataTransferItemList,
|
||||
// FileList lives on the factory slab and is frame-tracked, so each File ref it
|
||||
// holds is released at frame teardown (same path as `<input type=file>`).
|
||||
_files: *FileList,
|
||||
_drop_effect: []const u8 = "none",
|
||||
_effect_allowed: []const u8 = "uninitialized",
|
||||
|
||||
pub fn init(frame: *Frame) !*DataTransfer {
|
||||
const arena = try frame.getArena(.medium, "DataTransfer");
|
||||
errdefer frame.releaseArena(arena);
|
||||
|
||||
const fl = try frame._factory.create(FileList{});
|
||||
try frame.trackFileList(fl);
|
||||
|
||||
const self = try arena.create(DataTransfer);
|
||||
const list = try arena.create(DataTransferItemList);
|
||||
self.* = .{
|
||||
._arena = arena,
|
||||
._item_list = list,
|
||||
._files = fl,
|
||||
};
|
||||
list.* = .{ ._data_transfer = self };
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *DataTransfer, page: *Page) void {
|
||||
page.releaseArena(self._arena);
|
||||
}
|
||||
|
||||
pub fn acquireRef(self: *DataTransfer) void {
|
||||
self._rc.acquire();
|
||||
}
|
||||
|
||||
pub fn releaseRef(self: *DataTransfer, page: *Page) void {
|
||||
self._rc.release(self, page);
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/dnd.html#dom-datatransfer-getdata
|
||||
// "text" and "url" are shorthands the spec maps onto MIME types.
|
||||
fn normalizeFormat(arena: Allocator, format: []const u8) ![]const u8 {
|
||||
if (std.ascii.eqlIgnoreCase(format, "text")) {
|
||||
return "text/plain";
|
||||
}
|
||||
if (std.ascii.eqlIgnoreCase(format, "url")) {
|
||||
return "text/uri-list";
|
||||
}
|
||||
const buf = try arena.dupe(u8, format);
|
||||
return std.ascii.lowerString(buf, buf);
|
||||
}
|
||||
|
||||
pub fn getData(self: *const DataTransfer, format: []const u8, frame: *Frame) ![]const u8 {
|
||||
const norm = try normalizeFormat(frame.call_arena, format);
|
||||
for (self._items.items) |it| {
|
||||
if (it._kind == .string and std.mem.eql(u8, it._type, norm)) {
|
||||
return it._payload.string;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
pub fn setData(self: *DataTransfer, format: []const u8, data: []const u8) !void {
|
||||
const norm = try normalizeFormat(self._arena, format);
|
||||
const dup = try self._arena.dupe(u8, data);
|
||||
for (self._items.items) |it| {
|
||||
if (it._kind == .string and std.mem.eql(u8, it._type, norm)) {
|
||||
it._payload = .{ .string = dup };
|
||||
return;
|
||||
}
|
||||
}
|
||||
const it = try self._arena.create(DataTransferItem);
|
||||
it.* = .{ ._kind = .string, ._type = norm, ._payload = .{ .string = dup } };
|
||||
try self._items.append(self._arena, it);
|
||||
}
|
||||
|
||||
pub fn clearData(self: *DataTransfer, format_: ?[]const u8, frame: *Frame) !void {
|
||||
if (format_) |format| {
|
||||
const norm = try normalizeFormat(frame.call_arena, format);
|
||||
var i: usize = 0;
|
||||
while (i < self._items.items.len) {
|
||||
const it = self._items.items[i];
|
||||
if (it._kind == .string and std.mem.eql(u8, it._type, norm)) {
|
||||
_ = self._items.orderedRemove(i);
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
// No format: remove every string item, leave file items in place.
|
||||
var i: usize = 0;
|
||||
while (i < self._items.items.len) {
|
||||
if (self._items.items[i]._kind == .string) {
|
||||
_ = self._items.orderedRemove(i);
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- DataTransferItemList delegation ---
|
||||
|
||||
// add(File) -> file item ; add(DOMString, DOMString) -> string item.
|
||||
pub fn addItem(self: *DataTransfer, data: js.Value, type_: ?[]const u8, frame: *Frame) !?*DataTransferItem {
|
||||
if (data.toZig(*File)) |file| {
|
||||
return try self.addFileItem(file, frame);
|
||||
} else |_| {}
|
||||
|
||||
const s = try data.toZig([]const u8);
|
||||
const norm = try normalizeFormat(self._arena, type_ orelse "");
|
||||
const dup = try self._arena.dupe(u8, s);
|
||||
const it = try self._arena.create(DataTransferItem);
|
||||
it.* = .{ ._kind = .string, ._type = norm, ._payload = .{ .string = dup } };
|
||||
try self._items.append(self._arena, it);
|
||||
return it;
|
||||
}
|
||||
|
||||
fn addFileItem(self: *DataTransfer, file: *File, frame: *Frame) !*DataTransferItem {
|
||||
file._proto.acquireRef();
|
||||
const it = try self._arena.create(DataTransferItem);
|
||||
it.* = .{ ._kind = .file, ._type = file._proto.getType(), ._payload = .{ .file = file } };
|
||||
try self._items.append(self._arena, it);
|
||||
try self.rebuildFiles(frame);
|
||||
return it;
|
||||
}
|
||||
|
||||
pub fn removeItem(self: *DataTransfer, index: u32, frame: *Frame) !void {
|
||||
if (index >= self._items.items.len) {
|
||||
return;
|
||||
}
|
||||
const it = self._items.orderedRemove(index);
|
||||
if (it._kind == .file) {
|
||||
it._payload.file._proto.releaseRef(frame._page);
|
||||
try self.rebuildFiles(frame);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clearItems(self: *DataTransfer, frame: *Frame) !void {
|
||||
for (self._items.items) |it| {
|
||||
if (it._kind == .file) {
|
||||
it._payload.file._proto.releaseRef(frame._page);
|
||||
}
|
||||
}
|
||||
self._items.clearRetainingCapacity();
|
||||
try self.rebuildFiles(frame);
|
||||
}
|
||||
|
||||
// Rebuild the FileList slice from the current file-kind items, in order.
|
||||
fn rebuildFiles(self: *DataTransfer, frame: *Frame) !void {
|
||||
var files: std.ArrayList(*File) = .{};
|
||||
for (self._items.items) |it| {
|
||||
if (it._kind == .file) {
|
||||
try files.append(frame.arena, it._payload.file);
|
||||
}
|
||||
}
|
||||
self._files._files = try files.toOwnedSlice(frame.arena);
|
||||
}
|
||||
|
||||
// --- accessors ---
|
||||
|
||||
pub fn getFiles(self: *DataTransfer) *FileList {
|
||||
return self._files;
|
||||
}
|
||||
|
||||
pub fn getItems(self: *DataTransfer) *DataTransferItemList {
|
||||
return self._item_list;
|
||||
}
|
||||
|
||||
pub fn getTypes(self: *DataTransfer, frame: *Frame) ![][]const u8 {
|
||||
var out: std.ArrayList([]const u8) = .{};
|
||||
var has_files = false;
|
||||
for (self._items.items) |it| {
|
||||
switch (it._kind) {
|
||||
.string => try out.append(frame.call_arena, it._type),
|
||||
.file => has_files = true,
|
||||
}
|
||||
}
|
||||
if (has_files) {
|
||||
try out.append(frame.call_arena, "Files");
|
||||
}
|
||||
return out.toOwnedSlice(frame.call_arena);
|
||||
}
|
||||
|
||||
pub fn getDropEffect(self: *const DataTransfer) []const u8 {
|
||||
return self._drop_effect;
|
||||
}
|
||||
|
||||
pub fn setDropEffect(self: *DataTransfer, value: []const u8) !void {
|
||||
inline for (.{ "none", "copy", "link", "move" }) |valid| {
|
||||
if (std.mem.eql(u8, value, valid)) {
|
||||
self._drop_effect = valid;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getEffectAllowed(self: *const DataTransfer) []const u8 {
|
||||
return self._effect_allowed;
|
||||
}
|
||||
|
||||
pub fn setEffectAllowed(self: *DataTransfer, value: []const u8) !void {
|
||||
inline for (.{ "none", "copy", "copyLink", "copyMove", "link", "linkMove", "move", "all", "uninitialized" }) |valid| {
|
||||
if (std.mem.eql(u8, value, valid)) {
|
||||
self._effect_allowed = valid;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/dnd.html#dom-datatransfer-setdragimage
|
||||
// No-op: Lightpanda has no rendered drag feedback.
|
||||
pub fn setDragImage(_: *DataTransfer, _: js.Value, _: i32, _: i32) void {}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(DataTransfer);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "DataTransfer";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(DataTransfer.init, .{});
|
||||
pub const dropEffect = bridge.accessor(DataTransfer.getDropEffect, DataTransfer.setDropEffect, .{});
|
||||
pub const effectAllowed = bridge.accessor(DataTransfer.getEffectAllowed, DataTransfer.setEffectAllowed, .{});
|
||||
pub const files = bridge.accessor(DataTransfer.getFiles, null, .{});
|
||||
pub const items = bridge.accessor(DataTransfer.getItems, null, .{});
|
||||
pub const types = bridge.accessor(DataTransfer.getTypes, null, .{});
|
||||
pub const getData = bridge.function(DataTransfer.getData, .{});
|
||||
pub const setData = bridge.function(DataTransfer.setData, .{});
|
||||
pub const clearData = bridge.function(DataTransfer.clearData, .{});
|
||||
pub const setDragImage = bridge.function(DataTransfer.setDragImage, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "WebApi: DataTransfer" {
|
||||
try testing.htmlRunner("data_transfer.html", .{});
|
||||
}
|
||||
90
src/browser/webapi/DataTransferItem.zig
Normal file
90
src/browser/webapi/DataTransferItem.zig
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
|
||||
const File = @import("File.zig");
|
||||
|
||||
const log = lp.log;
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/dnd.html#the-datatransferitem-interface
|
||||
const DataTransferItem = @This();
|
||||
|
||||
pub const Kind = enum { string, file };
|
||||
|
||||
_kind: Kind,
|
||||
// For string items: the normalized format (e.g. "text/plain").
|
||||
// For file items: the File's MIME type.
|
||||
_type: []const u8,
|
||||
_payload: Payload,
|
||||
|
||||
pub const Payload = union(Kind) {
|
||||
string: []const u8,
|
||||
file: *File,
|
||||
};
|
||||
|
||||
pub fn getKind(self: *const DataTransferItem) []const u8 {
|
||||
return switch (self._kind) {
|
||||
.string => "string",
|
||||
.file => "file",
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getType(self: *const DataTransferItem) []const u8 {
|
||||
return self._type;
|
||||
}
|
||||
|
||||
pub fn getAsFile(self: *const DataTransferItem) ?*File {
|
||||
return switch (self._payload) {
|
||||
.file => |f| f,
|
||||
.string => null,
|
||||
};
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/dnd.html#dom-datatransferitem-getasstring
|
||||
// v1 invokes the callback synchronously with the string value. File items and a
|
||||
// missing callback are no-ops, per spec.
|
||||
pub fn getAsString(self: *const DataTransferItem, cb_: ?js.Function) !void {
|
||||
const cb = cb_ orelse return;
|
||||
const s = switch (self._payload) {
|
||||
.string => |str| str,
|
||||
.file => return,
|
||||
};
|
||||
var caught: js.TryCatch.Caught = undefined;
|
||||
cb.tryCall(void, .{s}, &caught) catch {
|
||||
log.debug(.js, "getAsString callback", .{ .caught = caught, .source = "DataTransferItem" });
|
||||
};
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(DataTransferItem);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "DataTransferItem";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const kind = bridge.accessor(DataTransferItem.getKind, null, .{});
|
||||
pub const @"type" = bridge.accessor(DataTransferItem.getType, null, .{});
|
||||
pub const getAsFile = bridge.function(DataTransferItem.getAsFile, .{});
|
||||
pub const getAsString = bridge.function(DataTransferItem.getAsString, .{});
|
||||
};
|
||||
104
src/browser/webapi/DataTransferItemList.zig
Normal file
104
src/browser/webapi/DataTransferItemList.zig
Normal file
@@ -0,0 +1,104 @@
|
||||
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const js = @import("../js/js.zig");
|
||||
const Frame = @import("../Frame.zig");
|
||||
|
||||
const DataTransfer = @import("DataTransfer.zig");
|
||||
const DataTransferItem = @import("DataTransferItem.zig");
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/dnd.html#the-datatransferitemlist-interface
|
||||
//
|
||||
// A live view over the owning DataTransfer's item list; all mutations are
|
||||
// delegated to the DataTransfer so `.files` stays in sync.
|
||||
const DataTransferItemList = @This();
|
||||
|
||||
_data_transfer: *DataTransfer,
|
||||
|
||||
pub fn getLength(self: *const DataTransferItemList) u32 {
|
||||
return @intCast(self._data_transfer._items.items.len);
|
||||
}
|
||||
|
||||
pub fn item(self: *const DataTransferItemList, index: u32) ?*DataTransferItem {
|
||||
const items = self._data_transfer._items.items;
|
||||
if (index >= items.len) {
|
||||
return null;
|
||||
}
|
||||
return items[index];
|
||||
}
|
||||
|
||||
// add(DOMString data, DOMString type) | add(File data)
|
||||
// The overload is resolved by inspecting the first argument: a File yields a
|
||||
// file item (the `type` argument is ignored), anything else a string item.
|
||||
pub fn add(self: *DataTransferItemList, data: js.Value, type_: ?[]const u8, frame: *Frame) !?*DataTransferItem {
|
||||
return self._data_transfer.addItem(data, type_, frame);
|
||||
}
|
||||
|
||||
pub fn remove(self: *DataTransferItemList, index: u32, frame: *Frame) !void {
|
||||
return self._data_transfer.removeItem(index, frame);
|
||||
}
|
||||
|
||||
pub fn clear(self: *DataTransferItemList, frame: *Frame) !void {
|
||||
return self._data_transfer.clearItems(frame);
|
||||
}
|
||||
|
||||
pub fn iterator(self: *DataTransferItemList, exec: *const js.Execution) !*Iterator {
|
||||
return Iterator.init(.{
|
||||
.index = 0,
|
||||
.list = self,
|
||||
}, exec);
|
||||
}
|
||||
|
||||
const GenericIterator = @import("collections/iterator.zig").Entry;
|
||||
pub const Iterator = GenericIterator(struct {
|
||||
index: u32,
|
||||
list: *DataTransferItemList,
|
||||
|
||||
pub fn next(self: *@This(), _: *const js.Execution) ?*DataTransferItem {
|
||||
const index = self.index;
|
||||
const it = self.list.item(index) orelse return null;
|
||||
self.index = index + 1;
|
||||
return it;
|
||||
}
|
||||
}, null);
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(DataTransferItemList);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "DataTransferItemList";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const length = bridge.accessor(DataTransferItemList.getLength, null, .{});
|
||||
pub const add = bridge.function(DataTransferItemList.add, .{});
|
||||
pub const remove = bridge.function(DataTransferItemList.remove, .{});
|
||||
pub const clear = bridge.function(DataTransferItemList.clear, .{});
|
||||
pub const @"[]" = bridge.indexed(DataTransferItemList.item, getIndexes, .{ .null_as_undefined = true });
|
||||
pub const symbol_iterator = bridge.iterator(DataTransferItemList.iterator, .{});
|
||||
|
||||
fn getIndexes(self: *DataTransferItemList, exec: *const js.Execution) !js.Array {
|
||||
const len = self.getLength();
|
||||
var arr = exec.js.local.?.newArray(len);
|
||||
for (0..len) |i| {
|
||||
_ = try arr.set(@intCast(i), i, .{});
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user