diff --git a/examples/js_dom_draw/README.md b/examples/js_dom_draw/README.md
new file mode 100644
index 0000000000..8baae5cb51
--- /dev/null
+++ b/examples/js_dom_draw/README.md
@@ -0,0 +1,7 @@
+Drawing with mouse events using DOM API. Adopted from MDN examples.
+
+# Compiling
+```
+v -b js_browser examples/js_dom_draw/draw.v
+```
+Then you can open `index.html` with your favourite browser.
diff --git a/examples/js_dom_draw/draw.js.v b/examples/js_dom_draw/draw.js.v
new file mode 100644
index 0000000000..32f85a3972
--- /dev/null
+++ b/examples/js_dom_draw/draw.js.v
@@ -0,0 +1,72 @@
+import jsdom
+import jsdom.ctx
+
+fn get_2dcontext(canvas jsdom.IElement) ?ctx.CanvasRenderingContext2D {
+ if canvas is jsdom.HTMLCanvasElement {
+ c := canvas.get_context('2d')
+ match c {
+ ctx.CanvasRenderingContext2D {
+ return c
+ }
+ else {
+ return error('cannot fetch 2d context')
+ }
+ }
+ } else {
+ return error('canvas is not an HTMLCanvasElement')
+ }
+}
+
+fn draw_line(context ctx.CanvasRenderingContext2D, x1 int, y1 int, x2 int, y2 int) {
+ context.begin_path()
+ context.set_stroke_style('black')
+ context.set_line_width(1)
+ context.move_to(x1, y1)
+ context.line_to(x2, y2)
+ context.stroke()
+ context.close_path()
+}
+
+fn main() {
+ document := jsdom.document
+
+ elem := document.get_element_by_id('myButton') ?
+ elemc := document.get_element_by_id('myCanvas') or { panic('no canvas') }
+ canv := jsdom.get_html_canvas_element(elemc) or { panic('expected canvas') }
+
+ context := get_2dcontext(canv) or { panic('wow') }
+ mut drawing := false
+ mut x := int(0)
+ mut y := int(0)
+ canv.add_event_listener('mousedown', fn [mut drawing, mut x, mut y] (_ jsdom.IEventTarget, event jsdom.IEvent) {
+ drawing = true
+ if event is jsdom.MouseEvent {
+ x = event.offset_x()
+ y = event.offset_y()
+ }
+ })
+
+ canv.add_event_listener('mousemove', fn [context, drawing, mut x, mut y] (_ jsdom.IEventTarget, event jsdom.IEvent) {
+ if drawing {
+ if event is jsdom.MouseEvent {
+ draw_line(context, x, y, event.offset_x(), event.offset_y())
+ x = event.offset_x()
+ y = event.offset_y()
+ }
+ }
+ })
+
+ jsdom.window.add_event_listener('mouseup', fn [context, mut drawing, mut x, mut y] (_ jsdom.IEventTarget, event jsdom.IEvent) {
+ if drawing {
+ if event is jsdom.MouseEvent {
+ draw_line(context, x, y, event.offset_x(), event.offset_y())
+ }
+ x = 0
+ y = 0
+ drawing = false
+ }
+ })
+ elem.add_event_listener('click', fn [context, canv] (_ jsdom.IEventTarget, _ jsdom.IEvent) {
+ context.clear_rect(0, 0, canv.width(), canv.height())
+ })
+}
diff --git a/examples/js_dom_draw/index.html b/examples/js_dom_draw/index.html
new file mode 100644
index 0000000000..3d60688a34
--- /dev/null
+++ b/examples/js_dom_draw/index.html
@@ -0,0 +1,7 @@
+
+ Drawing with mouse events
+
+
+
+
+
\ No newline at end of file
diff --git a/vlib/builtin/js/string.js.v b/vlib/builtin/js/string.js.v
index 18b11f1ab1..9e51b9a3c1 100644
--- a/vlib/builtin/js/string.js.v
+++ b/vlib/builtin/js/string.js.v
@@ -917,3 +917,10 @@ pub fn (_rune string) utf32_code() int {
return res
}
+
+pub fn tos(jsstr JS.String) string {
+ res := ''
+ #res.str = jsstr
+
+ return res
+}
diff --git a/vlib/jsdom/ctx/context.v b/vlib/jsdom/ctx/context.v
new file mode 100644
index 0000000000..cab5799b5a
--- /dev/null
+++ b/vlib/jsdom/ctx/context.v
@@ -0,0 +1,32 @@
+// Wrapper around 2d context and WebGL APIs
+
+module ctx
+
+pub struct ContextAttributes {
+pub:
+ alpha bool
+ desynchronized bool
+}
+
+pub enum PowerPreference {
+ default_
+ high_performance
+ low_performance
+}
+
+pub struct WebGLAttributes {
+pub:
+ alpha bool
+ desynchronized bool
+ antialias bool
+ depth bool
+ fail_if_major_perf_caveat bool
+ power_preference PowerPreference
+ premultiplied_alpha bool
+ preserve_drawing_buffer bool
+ stencil bool
+}
+
+pub struct NoneContext {}
+
+pub type ContextResult = CanvasRenderingContext2D | NoneContext | WebGLRenderingContext
diff --git a/vlib/jsdom/ctx/context_2d.js.v b/vlib/jsdom/ctx/context_2d.js.v
new file mode 100644
index 0000000000..e941e17ebc
--- /dev/null
+++ b/vlib/jsdom/ctx/context_2d.js.v
@@ -0,0 +1,67 @@
+module ctx
+
+struct JS.CanvasRenderingContext2D {
+mut:
+ lineWidth JS.Number
+ lineCap JS.String
+ lineJoin JS.String
+ miterLimit JS.Number
+ lineDashOffset JS.Number
+ font JS.String
+ textAlign JS.String
+ textBaseline JS.String
+ direction JS.String
+ fillStyle voidptr
+ strokeStyle voidptr
+ shadowBlur JS.Number
+ shadowColor JS.String
+ shadowOffsetX JS.Number
+ shadowOffsetY JS.Number
+ globalAlpha JS.Number
+ globalCompositeOperation JS.String
+}
+
+pub struct CanvasRenderingContext2D {
+ ctx JS.CanvasRenderingContext2D [noinit]
+}
+
+pub type StrokeStyle = string
+
+pub fn (ctx CanvasRenderingContext2D) begin_path() {
+ #ctx.ctx.beginPath();
+}
+
+pub fn (ctx CanvasRenderingContext2D) set_stroke_style(style StrokeStyle) {
+ #ctx.ctx.strokeStyle = style.str;
+}
+
+pub fn (ctx CanvasRenderingContext2D) line_width() int {
+ res := 0
+ #res.val = ctx.ctx.lineWidth
+
+ return res
+}
+
+pub fn (ctx CanvasRenderingContext2D) set_line_width(width int) {
+ #ctx.ctx.lineWidth = width.val
+}
+
+pub fn (ctx CanvasRenderingContext2D) move_to(x int, y int) {
+ #ctx.ctx.moveTo(x.val,y.val);
+}
+
+pub fn (ctx CanvasRenderingContext2D) line_to(x int, y int) {
+ #ctx.ctx.lineTo(x.val,y.val);
+}
+
+pub fn (ctx CanvasRenderingContext2D) stroke() {
+ #ctx.ctx.stroke();
+}
+
+pub fn (ctx CanvasRenderingContext2D) close_path() {
+ #ctx.ctx.closePath();
+}
+
+pub fn (ctx CanvasRenderingContext2D) clear_rect(x int, y int, width int, height int) {
+ #ctx.ctx.clearRect(x.val,y.val,width.val,height.val);
+}
diff --git a/vlib/jsdom/ctx/webgl.js.v b/vlib/jsdom/ctx/webgl.js.v
new file mode 100644
index 0000000000..969829bb51
--- /dev/null
+++ b/vlib/jsdom/ctx/webgl.js.v
@@ -0,0 +1,3 @@
+module ctx
+
+pub struct WebGLRenderingContext {}
diff --git a/vlib/jsdom/document.js.v b/vlib/jsdom/document.js.v
new file mode 100644
index 0000000000..d65e24c3a8
--- /dev/null
+++ b/vlib/jsdom/document.js.v
@@ -0,0 +1,230 @@
+module jsdom
+
+pub struct JS.Document {
+}
+
+pub struct Document {
+ Node
+}
+
+pub struct Location {
+mut:
+ loc JS.Location [noinit]
+}
+
+pub fn (l Location) str() string {
+ mut res := 'Location{\n'
+ res += ' origin: $l.origin()\n'
+ res += ' href: $l.href()\n'
+ res += ' protocol: $l.protocol()\n'
+ res += ' host: $l.host()\n'
+ res += ' hostname: $l.hostname()\n'
+ res += ' port: $l.port()\n'
+ res += ' pathname: $l.pathname()\n'
+ res += ' search: $l.search()\n'
+ res += ' hash: $l.hash()\n'
+ return res
+}
+
+pub fn (mut l Location) assign(url string) {
+ #l.val.loc.assign(url.str)
+}
+
+pub fn (l Location) reload() {
+ #l.loc.reload()
+}
+
+pub fn (mut l Location) replace(url string) {
+ #l.val.loc.replace(url.str)
+}
+
+pub fn (l Location) origin() string {
+ return tos(l.loc.origin)
+}
+
+pub fn (l Location) href() string {
+ return tos(l.loc.href)
+}
+
+pub fn (mut l Location) set_href(href string) {
+ l.loc.href = href.str
+}
+
+pub fn (l Location) protocol() string {
+ return tos(l.loc.protocol)
+}
+
+pub fn (mut l Location) set_protocol(protocol string) {
+ l.loc.protocol = protocol.str
+}
+
+pub fn (l Location) host() string {
+ return tos(l.loc.host)
+}
+
+pub fn (mut l Location) set_host(host string) {
+ l.loc.host = host.str
+}
+
+pub fn (l Location) hostname() string {
+ return tos(l.loc.hostname)
+}
+
+pub fn (mut l Location) set_hostname(hostname string) {
+ l.loc.hostname = hostname.str
+}
+
+pub fn (l Location) port() string {
+ return tos(l.loc.port)
+}
+
+pub fn (mut l Location) set_port(port string) {
+ l.loc.port = port.str
+}
+
+pub fn (l Location) pathname() string {
+ return tos(l.loc.pathname)
+}
+
+pub fn (mut l Location) set_pathname(pathname string) {
+ l.loc.pathname = pathname.str
+}
+
+pub fn (l Location) hash() string {
+ return tos(l.loc.hash)
+}
+
+pub fn (mut l Location) set_hash(hash string) {
+ l.loc.hash = hash.str
+}
+
+pub fn (l Location) search() string {
+ return tos(l.loc.search)
+}
+
+pub fn (mut l Location) set_search(search string) {
+ l.loc.search = search.str
+}
+
+pub struct JS.Location {
+pub:
+ origin JS.String
+mut:
+ href JS.String
+ protocol JS.String
+ host JS.String
+ hostname JS.String
+ port JS.String
+ pathname JS.String
+ search JS.String
+ hash JS.String
+}
+
+pub fn (doc Document) active_element() Element {
+ mut elem := Element{}
+ #elem.node = doc.node.activeElement;
+
+ return elem
+}
+
+pub fn (doc Document) get(name string) ?Element {
+ mut elem := Element{}
+ #elem.node = doc.node[name.str];
+ #console.log(elem.node)
+ #if (elem.node === null || elem.node === undefined) return new $ref(new Option({state: new byte(2),err: none__}));
+
+ return elem
+}
+
+// location returns URI of the document
+pub fn (doc Document) location() Location {
+ mut loc := Location{}
+ #loc.loc = doc.node.location;
+
+ return loc
+}
+
+// get_title returns current title of document
+pub fn (doc Document) get_title() string {
+ res := ''
+ #res.str = doc.node.title;
+
+ return res
+}
+
+// set_title updates document title
+pub fn (doc Document) set_title(title string) {
+ #doc.node.title = title.str;
+}
+
+// url returns document location as a string
+pub fn (doc Document) url() string {
+ res := ''
+ #res.str = doc.node.URL;
+
+ return res
+}
+
+// node casts `Document` back to `Node`.
+pub fn (doc Document) node() Node {
+ node := Node{}
+ #node.node = doc.node
+
+ return node
+}
+
+pub fn (doc Document) get_element_by_id(id string) ?IElement {
+ mut elem := IElement(Element{})
+ found := false
+ #let tmp = doc.node.getElementById(id.str);
+ #elem = jsdom__dispatch_event_target(tmp);
+ #found.val = !(elem.node == null)
+ if !found {
+ return none
+ }
+ return elem
+}
+
+pub type DocumentPrepend = Node | string
+
+pub fn (doc Document) prepend(nodes_or_strings ...DocumentPrepend) ? {
+ caught := false
+ err := ''
+ #try {
+
+ for elem in nodes_or_strings {
+ match elem {
+ string {
+ #doc.node.prepend(elem.str)
+ }
+ Node {
+ #doc.node.prepend(elem.node)
+ }
+ }
+ }
+ #} catch (e) { caught.val = true; err.str = e.toString(); }
+ if caught {
+ return error(err)
+ }
+}
+
+pub fn (doc Document) create_element(tag_name string) Element {
+ elem := Element{}
+ #elem.node = doc.node.createElement(tag_name.str)
+
+ return elem
+}
+
+pub fn get_document() Document {
+ doc := Document{}
+ #doc.node = document;
+
+ return doc
+}
+
+pub fn (elem Document) add_event_listener(event string, cb EventCallback) {
+ #elem.node.addEventListener(event.str, function (event) { let e = jsdom__dispatch_event_target(this);
+ #let ev = jsdom__dispatch_event(event); ev.event = event;
+ #return cb(e,ev)
+ #});
+}
diff --git a/vlib/jsdom/dom.js.v b/vlib/jsdom/dom.js.v
new file mode 100644
index 0000000000..419102696b
--- /dev/null
+++ b/vlib/jsdom/dom.js.v
@@ -0,0 +1,186 @@
+// DOM API for JS backend
+module jsdom
+
+[heap]
+pub struct JS.Node {
+ baseURI JS.String [noinit]
+ childNodes JS.NodeList [noinit]
+ firstChild voidptr [noinit]
+ isConnected JS.bool [noinit]
+ lastChild voidptr [noinit]
+ nextSibling voidptr [noinit]
+ nodeName JS.String [noinit]
+ nodeType NodeType [noinit]
+ nodeValue voidptr [noinit]
+ ownerDocument JS.Document [noinit]
+ parentNode voidptr [noinit]
+ parentElement JS.Element [noinit]
+ previousSibling voidptr [noinit]
+ textContext voidptr
+}
+
+pub struct JS.NodeList {
+pub mut:
+ length JS.Number
+}
+
+pub enum NodeType {
+ element = 1
+ attribute = 2
+ text = 3
+ cdata_section = 4
+ entity_reference = 5
+ entity = 6
+ processing_instruction = 7
+ comment = 8
+ document = 9
+ document_type = 10
+ document_fragment = 11
+ notation = 12
+}
+
+pub struct Node {
+ node JS.Node [noinit]
+}
+
+pub fn (n Node) typ() NodeType {
+ return n.node.nodeType
+}
+
+/// IEventTarget interface is implemented by objects that can receive events and may have listeners for them.
+// In other words, any target of events implements the three methods associated with this interface.
+// TODO: remove_event_listener and dispatch_event
+pub interface IEventTarget {
+ add_event_listener(event string, cb EventCallback)
+}
+
+/// The DOM Node interface is an abstract base class upon which many other DOM API objects are based, thus
+// letting those object types to be used similarly and often interchangeably. As an abstract class,
+// there is no such thing as a plain Node object. All objects that implement Node functionality are based on one of its subclasses.
+// Most notable are Document, Element, and DocumentFragment.
+pub interface INode {
+ IEventTarget
+ typ() NodeType
+}
+
+pub fn (elem Node) add_event_listener(event string, cb EventCallback) {
+ #elem.node.addEventListener(event.str, function (event) { let e = jsdom__dispatch_event_target(this);
+ #let ev = jsdom__dispatch_event(event); ev.event = event;
+ #return cb(e,ev)
+ #});
+}
+
+pub fn (n INode) is_(ty NodeType) bool {
+ res := false
+ #res.val = n.node.nodeType == ty
+
+ return res
+}
+
+pub fn (n INode) document() ?Document {
+ res := Document{}
+ if n.is_(.document) {
+ #res.node = n.node
+
+ return res
+ } else {
+ return none
+ }
+}
+
+pub fn (n INode) element() ?Element {
+ res := Element{}
+ if n.is_(.element) {
+ #res.node = n.node
+
+ return res
+ } else {
+ return none
+ }
+}
+
+pub fn (n INode) append_child(child INode) {
+ #n.node.appendChild(child.node)
+}
+
+pub fn (n INode) clone_node(deep ...int) INode {
+ cloned := Node{}
+ if deep.len == 0 {
+ #cloned.node = n.node.cloneNode()
+ } else {
+ #cloned.node = n.node.cloneNode(deep.arr.get(new int(0)).val)
+ }
+ return cloned
+}
+
+pub fn (n INode) contains(other INode) bool {
+ res := false
+ #res.val = n.node.contains(other.node)
+
+ return res
+}
+
+pub fn (n INode) get_root_node() INode {
+ root := Node{}
+ #root.node = n.node.getRootNode()
+
+ return root
+}
+
+pub fn (n INode) has_child_nodes() bool {
+ res := false
+ #res.val = n.node.hasChildNodes()
+
+ return res
+}
+
+pub fn (n INode) insert_before(new_node INode, reference_node INode) Node {
+ inserted := Node{}
+ #inserted.node = n.node.insertBefore(new_node.node,reference_node.node)
+
+ return inserted
+}
+
+/*
+pub fn (x Node) == (y Node) bool {
+ res := false
+ #res.val = x.node.isEqualNode(y.node)
+
+ return res
+}*/
+
+pub fn (n INode) remove_child(child INode) {
+ #n.node.removeChild(child.node)
+}
+
+pub fn (n INode) replace_child(new_child INode, old_child INode) INode {
+ #old_child.node = n.node.replace_child(new_child.node,old_child.node)
+
+ return old_child
+}
+
+pub struct JS.EventTarget {}
+
+fn dispatch_event_target(target JS.EventTarget) IEventTarget {
+ mut ret := IEventTarget(Element{})
+ #if (target instanceof HTMLCanvasElement) { ret = new jsdom__HTMLCanvasElement({}); ret.node = target; }
+ #else if (target instanceof HTMLElement) { ret = new jsdom__HTMLElement({}); ret.node = target; }
+ #else if (target instanceof Window) { ret = new jsdom__Window({}); ret.node = target;}
+ #else if (target instanceof SVGElement) { ret = new jsdom__SVGElement({}); ret.node = target; }
+ #else if (target instanceof Element) { ret = new jsdom__Element({}); ret.node = target; }
+ #else if (target instanceof Document) { ret = new jsdom__Document({}); ret.node = target; }
+
+ return ret
+}
+
+pub type EventCallback = fn (_ IEventTarget, _ IEvent)
+
+pub const (
+ document = Document{}
+ window = Window{}
+)
+
+fn init() {
+ #jsdom__document.node = document;
+ #jsdom__window.node = window;
+}
diff --git a/vlib/jsdom/dom.v b/vlib/jsdom/dom.v
new file mode 100644
index 0000000000..5d5139ebad
--- /dev/null
+++ b/vlib/jsdom/dom.v
@@ -0,0 +1,10 @@
+// DOM API wrapper for JS backend
+module jsdom
+
+pub fn get_html_canvas_element(elem IElement) ?HTMLCanvasElement {
+ if elem is HTMLCanvasElement {
+ return *elem
+ } else {
+ return none
+ }
+}
diff --git a/vlib/jsdom/dom_token_list.js.v b/vlib/jsdom/dom_token_list.js.v
new file mode 100644
index 0000000000..37876fddf6
--- /dev/null
+++ b/vlib/jsdom/dom_token_list.js.v
@@ -0,0 +1,97 @@
+module jsdom
+
+pub struct JS.DOMString {
+}
+
+pub struct JS.DOMTokenList {
+ length JS.Number
+ value JS.DOMString
+}
+
+pub struct DOMTokenList {
+ list JS.DOMTokenList [noinit]
+}
+
+pub fn (x DOMTokenList) len() int {
+ res := 0
+ #res.val = x.list.length;
+
+ return res
+}
+
+pub fn (x DOMTokenList) item(idx int) ?string {
+ res := ''
+ #let tmp = x.list.item(idx.list.val)
+ #if (tmp === undefined) return new Option({state: new byte(2),err: none__});
+ #res.val = tmp
+
+ return res
+}
+
+pub fn (x DOMTokenList) contains(token string) bool {
+ res := false
+ #res.val = x.list.contains(token.str);
+
+ return res
+}
+
+pub fn (x DOMTokenList) add(tokens ...string) {
+ for token in tokens {
+ #x.list.add(token.str);
+
+ _ := token
+ }
+}
+
+pub fn (x DOMTokenList) remove(tokens ...string) {
+ for token in tokens {
+ #x.list.remove(token.str);
+
+ _ := token
+ }
+}
+
+pub fn (x DOMTokenList) replace(old_token string, new_token string) bool {
+ is_replaced := false
+ #is_replaced.val = x.list.replace(old_token.str,new_token.str);
+
+ return is_replaced
+}
+
+// supports returns true if the given `token` is in the associated attibute's supported tokens.
+pub fn (x DOMTokenList) supports(token string) bool {
+ supports := false
+ #supports.val = x.list.supports(token.str)
+
+ return supports
+}
+
+// toggle removes a given token from the list and returns `false`. If token does not exist
+// it is added and function returns `true`.
+pub fn (x DOMTokenList) toggle(token string, force bool) bool {
+ res := false
+ #res.val = x.list.toggle(token.str, force.val);
+
+ return res
+}
+
+// entries returns array of all tokens in this token list
+pub fn (x DOMTokenList) values() []string {
+ mut res := []string{}
+ #for (let [_,value] of x.list.entries()) array_push(res, new string(value));
+
+ return res
+}
+
+pub fn (x DOMTokenList) str() string {
+ mut fmt := 'DOMTokenList['
+ values := x.values()
+ for i, val in values {
+ fmt += '"' + val + '"'
+ if i != values.len - 1 {
+ fmt += ','
+ }
+ }
+ fmt += ']'
+ return fmt
+}
diff --git a/vlib/jsdom/element.js.v b/vlib/jsdom/element.js.v
new file mode 100644
index 0000000000..74f0aa69bf
--- /dev/null
+++ b/vlib/jsdom/element.js.v
@@ -0,0 +1,93 @@
+module jsdom
+
+pub struct JS.Element {
+ classList JS.DOMTokenList
+ childElementCount JS.Number
+ className JS.String
+ clientHeight JS.Number
+ clientWidth JS.Number
+ clientTop JS.Number
+ clientLeft JS.Number
+ id JS.String
+ innerHTML JS.String
+ namespaceURI JS.String
+ outerHTML JS.String
+ scrollHeight JS.Number
+ scrollLeft JS.Number
+ scrollTop JS.Number
+ scrollWidth JS.Number
+}
+
+pub struct Element {
+ Node
+}
+
+pub fn (e Element) str() string {
+ res := ''
+ #res.str = e.node + ''
+
+ return res
+}
+
+pub fn (elem Element) typ() NodeType {
+ return .element
+}
+
+pub fn (e Element) class_name() string {
+ res := ''
+ #res.str = e.node.className
+
+ return res
+}
+
+pub fn (e Element) class_list() DOMTokenList {
+ list := DOMTokenList{}
+ #list.list = e.node.classList
+
+ return list
+}
+
+// node casts `Element` back to `Node`.
+pub fn (elem Element) node() Node {
+ node := Node{}
+ #node.node = elem.node
+
+ return node
+}
+
+pub fn (elem Element) add_event_listener(event string, cb EventCallback) {
+ #elem.node.addEventListener(event.str, function (event) { let e = jsdom__dispatch_event_target(this);
+ #let ev = jsdom__dispatch_event(event); ev.event = event;
+ #return cb(e,ev)
+ #});
+}
+
+pub interface IElement {
+ INode
+}
+
+pub struct HTMLElement {
+ Element
+}
+
+pub fn (elem HTMLElement) typ() NodeType {
+ return .element
+}
+
+pub fn (elem HTMLElement) access_key() string {
+ res := ''
+ #res.str = elem.node.accessKey;
+
+ return res
+}
+
+pub fn (mut elem HTMLElement) set_access_key(key string) {
+ #elem.val.node.accessKey = key.str;
+}
+
+pub fn (elem HTMLElement) add_event_listener(event string, cb EventCallback) {
+ #elem.node.addEventListener(event.str, function (event) { let e = jsdom__dispatch_event_target(this);
+ #let ev = jsdom__dispatch_event(event); ev.event = event;
+ #return cb(e,ev)
+ #});
+}
diff --git a/vlib/jsdom/events.js.v b/vlib/jsdom/events.js.v
new file mode 100644
index 0000000000..2f4b3251fd
--- /dev/null
+++ b/vlib/jsdom/events.js.v
@@ -0,0 +1,164 @@
+module jsdom
+
+pub interface IEvent {
+ composed_path() []IEventTarget
+}
+
+pub struct JS.Event {}
+
+pub struct JS.MouseEvent {}
+
+pub struct Event {
+ event JS.Event [noinit]
+}
+
+pub fn (ev Event) composed_path() []IEventTarget {
+ mut composed := []IEventTarget{}
+ #ev.event.composedPath().forEach((item) => {
+ #array_push(composed, jsdom__dispatch_event())
+ #})
+
+ return composed
+}
+
+pub struct UIEvent {
+ Event
+}
+
+// detail returns `int` with detail about the event, depending on the event type.
+pub fn (ev UIEvent) detail() int {
+ ret := 0
+ #ret.val = ev.event.detail
+
+ return ret
+}
+
+pub struct MouseEvent {
+ UIEvent
+}
+
+// button returns the button number that was pressed (if applicable) when the mouse event was fired.
+pub fn (ev MouseEvent) button() int {
+ ret := 0
+ #ret.val = ev.event.button;
+
+ return ret
+}
+
+// alt_key returns `true` if the `alt` key was down when the mouse event was fired.
+pub fn (ev MouseEvent) alt_key() bool {
+ ret := false
+ #ret.val = ev.event.altKey;
+
+ return ret
+}
+
+// buttons returns the buttons being depressed (if any) when the mouse event was fired.
+pub fn (ev MouseEvent) buttons() int {
+ ret := 0
+ #ret.val = ev.event.buttons;
+
+ return ret
+}
+
+// client_x returns the X coordinate of the mouse pointer in local coordinates.
+pub fn (ev MouseEvent) client_x() int {
+ ret := 0
+ #ret.val = ev.event.clientX;
+
+ return ret
+}
+
+// client_y returns the Y coordinate of the mouse pointer in local coordinates.
+pub fn (ev MouseEvent) client_y() int {
+ ret := 0
+ #ret.val = ev.event.clientY;
+
+ return ret
+}
+
+// ctrl_key returns `true` if the `ctrl` key was down when the mouse event was fired.
+pub fn (ev MouseEvent) ctrl_key() bool {
+ ret := false
+ #ret.val = ev.event.ctrlKey;
+
+ return ret
+}
+
+// meta_key returns `true` if the `meta` key was down when the mouse event was fired.
+pub fn (ev MouseEvent) meta_key() bool {
+ ret := false
+ #ret.val = ev.event.metaKey;
+
+ return ret
+}
+
+// movenet_x reaturns the X coordinate of the mouse pointer relative to the position of the last `mousemove` event.
+pub fn (ev MouseEvent) movement_x() int {
+ ret := 0
+ #ret.val = ev.event.movementX;
+
+ return ret
+}
+
+// movenet_y reaturns the Y coordinate of the mouse pointer relative to the position of the last `mousemove` event.
+pub fn (ev MouseEvent) movement_y() int {
+ ret := 0
+ #ret.val = ev.event.movementY;
+
+ return ret
+}
+
+// offset_x returns the X coordinate of the mouse pointer relative to the position of the padding edge of the target node.
+pub fn (ev MouseEvent) offset_x() int {
+ ret := 0
+ #ret.val = ev.event.offsetX;
+
+ return ret
+}
+
+// offset_y reaturns the Y coordinate of the mouse pointer relative to the position of the padding edge of the target node.
+pub fn (ev MouseEvent) offset_y() int {
+ ret := 0
+ #ret.val = ev.event.offsetY;
+
+ return ret
+}
+
+pub fn (ev MouseEvent) composed_path() []IEventTarget {
+ mut composed := []IEventTarget{}
+ #ev.event.composedPath().forEach((item) => {
+ #array_push(composed, jsdom__dispatch_event())
+ #})
+
+ return composed
+}
+
+pub struct AbortSignal {
+ Event
+}
+
+pub fn (ev AbortSignal) composed_path() []IEventTarget {
+ mut composed := []IEventTarget{}
+ #ev.event.composedPath().forEach((item) => {
+ #array_push(composed, jsdom__dispatch_event(item))
+ #})
+
+ return composed
+}
+
+pub fn (sig AbortSignal) aborted() bool {
+ res := false
+ #res.val = sig.event.aborted;
+
+ return res
+}
+
+fn dispatch_event(event JS.Event) IEvent {
+ mut ret := IEvent(Event{})
+ #if (event instanceof AbortSignal) { ret = new jsdom__AbortSignal({}); }
+ #else if (event instanceof MouseEvent) { ret = new jsdom__MouseEvent({}); }
+ #ret.event = event
+
+ return ret
+}
diff --git a/vlib/jsdom/html_canvas_element.js.v b/vlib/jsdom/html_canvas_element.js.v
new file mode 100644
index 0000000000..adce1a55aa
--- /dev/null
+++ b/vlib/jsdom/html_canvas_element.js.v
@@ -0,0 +1,40 @@
+module jsdom
+
+import jsdom.ctx
+
+pub struct HTMLCanvasElement {
+ HTMLElement
+}
+
+pub fn (cv HTMLCanvasElement) height() int {
+ ret := 0
+ #ret.val = cv.node.height;
+
+ return ret
+}
+
+pub fn (cv HTMLCanvasElement) width() int {
+ ret := 0
+ #ret.val = cv.node.width;
+
+ return ret
+}
+
+pub fn (cv HTMLCanvasElement) typ() NodeType {
+ return .element
+}
+
+pub fn (elem HTMLCanvasElement) add_event_listener(event string, cb EventCallback) {
+ #elem.node.addEventListener(event.str, function (event) { let e = jsdom__dispatch_event_target(this);
+ #let ev = jsdom__dispatch_event(event); ev.event = event;
+ #return cb(e,ev)
+ #});
+}
+
+pub fn (elem HTMLCanvasElement) get_context(ctx_ string) ctx.ContextResult {
+ mut res := ctx.NoneContext{}
+ #let ctx = elem.node.getContext(ctx_.str);
+ #if (ctx instanceof CanvasRenderingContext2D) { res = new jsdom__ctx__CanvasRenderingContext2D(ctx); res.ctx = ctx; }
+
+ return res
+}
diff --git a/vlib/jsdom/window.js.v b/vlib/jsdom/window.js.v
new file mode 100644
index 0000000000..dfecd9d0ce
--- /dev/null
+++ b/vlib/jsdom/window.js.v
@@ -0,0 +1,15 @@
+module jsdom
+
+pub struct JS.Window {
+}
+
+pub struct Window {
+ node JS.Window [noinit]
+}
+
+pub fn (elem Window) add_event_listener(event string, cb EventCallback) {
+ #elem.node.addEventListener(event.str, function (event) { let e = jsdom__dispatch_event_target(this);
+ #let ev = jsdom__dispatch_event(event); ev.event = event;
+ #return cb(e,ev)
+ #});
+}
diff --git a/vlib/v/gen/js/fn.v b/vlib/v/gen/js/fn.v
index 6e9e266d26..e7356e8f75 100644
--- a/vlib/v/gen/js/fn.v
+++ b/vlib/v/gen/js/fn.v
@@ -164,7 +164,7 @@ fn (mut g JsGen) method_call(node ast.CallExpr) {
g.expr(it.left)
mut ltyp := it.left_type
for ltyp.is_ptr() {
- g.write('.val')
+ g.write('.valueOf()')
ltyp = ltyp.deref()
}
g.write('.')
diff --git a/vlib/v/gen/js/js.v b/vlib/v/gen/js/js.v
index 5293d3b87a..0fdf02dc29 100644
--- a/vlib/v/gen/js/js.v
+++ b/vlib/v/gen/js/js.v
@@ -2718,7 +2718,7 @@ fn (mut g JsGen) gen_index_expr(expr ast.IndexExpr) {
fn (mut g JsGen) gen_deref_ptr(ty ast.Type) {
mut t := ty
for t.is_ptr() {
- g.write('.val')
+ g.write('.valueOf()')
t = t.deref()
}
}