package plugin_ui import ( "seanime/internal/util/result" "github.com/dop251/goja" "github.com/google/uuid" ) // DOMManager handles DOM manipulation requests from plugins type DOMManager struct { ctx *Context elementObservers *result.Map[string, *ElementObserver] eventListeners *result.Map[string, *DOMEventListener] } type ElementObserver struct { ID string Selector string Callback goja.Callable } type DOMEventListener struct { ID string ElementId string EventType string Callback goja.Callable } // NewDOMManager creates a new DOM manager func NewDOMManager(ctx *Context) *DOMManager { return &DOMManager{ ctx: ctx, elementObservers: result.NewResultMap[string, *ElementObserver](), eventListeners: result.NewResultMap[string, *DOMEventListener](), } } // BindToObj binds DOM manipulation methods to a context object func (d *DOMManager) BindToObj(vm *goja.Runtime, obj *goja.Object) { domObj := vm.NewObject() _ = domObj.Set("query", d.jsQuery) _ = domObj.Set("queryOne", d.jsQueryOne) _ = domObj.Set("observe", d.jsObserve) _ = domObj.Set("observeInView", d.jsObserveInView) _ = domObj.Set("createElement", d.jsCreateElement) _ = domObj.Set("asElement", d.jsAsElement) _ = domObj.Set("onReady", d.jsOnReady) _ = obj.Set("dom", domObj) } func (d *DOMManager) jsOnReady(call goja.FunctionCall) goja.Value { callback, ok := goja.AssertFunction(call.Argument(0)) if !ok { d.ctx.handleTypeError("onReady requires a callback function") } // Listen for changes from the client listener := d.ctx.RegisterEventListener(ClientDOMReadyEvent) listener.SetCallback(func(event *ClientPluginEvent) { d.ctx.scheduler.ScheduleAsync(func() error { _, err := callback(goja.Undefined(), d.ctx.vm.ToValue(event.Payload)) if err != nil { d.ctx.handleException(err) } return nil }) }) return d.ctx.vm.ToValue(nil) } // jsQuery handles querying for multiple DOM elements func (d *DOMManager) jsQuery(call goja.FunctionCall) goja.Value { selector := call.Argument(0).String() promise, resolve, _ := d.ctx.vm.NewPromise() // Generate a unique request ID requestId := uuid.New().String() opts := d.getQueryElementOptions(call.Argument(1)) // Set up a one-time event listener for the response listener := d.ctx.RegisterEventListener(ClientDOMQueryResultEvent) listener.SetCallback(func(event *ClientPluginEvent) { var payload ClientDOMQueryResultEventPayload if event.ParsePayloadAs(ClientDOMQueryResultEvent, &payload) && payload.RequestID == requestId { d.ctx.scheduler.ScheduleAsync(func() error { elemObjs := make([]interface{}, 0, len(payload.Elements)) for _, elem := range payload.Elements { if elemData, ok := elem.(map[string]interface{}); ok { elemObjs = append(elemObjs, d.createDOMElementObject(elemData)) } } resolve(d.ctx.vm.ToValue(elemObjs)) return nil }) d.ctx.UnregisterEventListener(listener.ID) } }) // Send the query request to the client d.ctx.SendEventToClient(ServerDOMQueryEvent, &ServerDOMQueryEventPayload{ Selector: selector, RequestID: requestId, WithInnerHTML: opts.WithInnerHTML, WithOuterHTML: opts.WithOuterHTML, IdentifyChildren: opts.IdentifyChildren, }) return d.ctx.vm.ToValue(promise) } // jsQueryOne handles querying for a single DOM element func (d *DOMManager) jsQueryOne(call goja.FunctionCall) goja.Value { selector := call.Argument(0).String() promise, resolve, _ := d.ctx.vm.NewPromise() // Generate a unique request ID requestId := uuid.New().String() opts := d.getQueryElementOptions(call.Argument(1)) // Set up a one-time event listener for the response listener := d.ctx.RegisterEventListener(ClientDOMQueryOneResultEvent) listener.SetCallback(func(event *ClientPluginEvent) { var payload ClientDOMQueryOneResultEventPayload if event.ParsePayloadAs(ClientDOMQueryOneResultEvent, &payload) && payload.RequestID == requestId { d.ctx.scheduler.ScheduleAsync(func() error { if payload.Element != nil { if elemData, ok := payload.Element.(map[string]interface{}); ok { resolve(d.ctx.vm.ToValue(d.createDOMElementObject(elemData))) } else { resolve(d.ctx.vm.ToValue(goja.Null())) } } else { resolve(d.ctx.vm.ToValue(goja.Null())) } return nil }) d.ctx.UnregisterEventListener(listener.ID) } }) // Send the query request to the client d.ctx.SendEventToClient(ServerDOMQueryOneEvent, &ServerDOMQueryOneEventPayload{ Selector: selector, RequestID: requestId, WithInnerHTML: opts.WithInnerHTML, WithOuterHTML: opts.WithOuterHTML, IdentifyChildren: opts.IdentifyChildren, }) return d.ctx.vm.ToValue(promise) } type QueryElementOptions struct { WithInnerHTML bool `json:"withInnerHTML"` WithOuterHTML bool `json:"withOuterHTML"` IdentifyChildren bool `json:"identifyChildren"` } func (d *DOMManager) getQueryElementOptions(argument goja.Value) QueryElementOptions { options := QueryElementOptions{ WithInnerHTML: false, WithOuterHTML: false, IdentifyChildren: false, } if argument != goja.Undefined() && argument != goja.Null() { optsObj, ok := argument.Export().(map[string]interface{}) if !ok { d.ctx.handleTypeError("third argument 'opts' must be an object") } // Extract 'withInnerHTML' from 'opts' if present if val, exists := optsObj["withInnerHTML"]; exists { options.WithInnerHTML, ok = val.(bool) if !ok { d.ctx.handleTypeError("'withInnerHTML' property must be a boolean") } } // Extract 'identifyChildren' from 'opts' if present if val, exists := optsObj["identifyChildren"]; exists { options.IdentifyChildren, ok = val.(bool) if !ok { d.ctx.handleTypeError("'identifyChildren' property must be a boolean") } } // Extract 'withOuterHTML' from 'opts' if present if val, exists := optsObj["withOuterHTML"]; exists { options.WithOuterHTML, ok = val.(bool) if !ok { d.ctx.handleTypeError("'withOuterHTML' property must be a boolean") } } } return options } // jsObserve starts observing DOM elements matching a selector func (d *DOMManager) jsObserve(call goja.FunctionCall) goja.Value { selector := call.Argument(0).String() callback, ok := goja.AssertFunction(call.Argument(1)) if !ok { d.ctx.handleTypeError("observe requires a callback function") } options := d.getQueryElementOptions(call.Argument(2)) // Create observer ID observerId := uuid.New().String() // Store the observer observer := &ElementObserver{ ID: observerId, Selector: selector, Callback: callback, } d.elementObservers.Set(observerId, observer) // Send observe request to client d.ctx.SendEventToClient(ServerDOMObserveEvent, &ServerDOMObserveEventPayload{ Selector: selector, ObserverId: observerId, WithInnerHTML: options.WithInnerHTML, WithOuterHTML: options.WithOuterHTML, IdentifyChildren: options.IdentifyChildren, }) // Start a goroutine to handle observer updates listener := d.ctx.RegisterEventListener(ClientDOMObserveResultEvent) listener.SetCallback(func(event *ClientPluginEvent) { var payload ClientDOMObserveResultEventPayload if event.ParsePayloadAs(ClientDOMObserveResultEvent, &payload) && payload.ObserverId == observerId { d.ctx.scheduler.ScheduleAsync(func() error { observer, exists := d.elementObservers.Get(observerId) if !exists { return nil } // Convert elements to DOM element objects directly in the VM thread elemObjs := make([]interface{}, 0, len(payload.Elements)) for _, elem := range payload.Elements { if elemData, ok := elem.(map[string]interface{}); ok { elemObjs = append(elemObjs, d.createDOMElementObject(elemData)) } } // Call the callback directly now that we have all elements _, err := observer.Callback(goja.Undefined(), d.ctx.vm.ToValue(elemObjs)) if err != nil { d.ctx.handleException(err) } return nil }) } }) // Listen for DOM ready events to re-observe elements after page reload domReadyListener := d.ctx.RegisterEventListener(ClientDOMReadyEvent) domReadyListener.SetCallback(func(event *ClientPluginEvent) { // Re-send the observe request when the DOM is ready d.ctx.SendEventToClient(ServerDOMObserveEvent, &ServerDOMObserveEventPayload{ Selector: selector, ObserverId: observerId, WithInnerHTML: options.WithInnerHTML, WithOuterHTML: options.WithOuterHTML, IdentifyChildren: options.IdentifyChildren, }) }) // Return a function to stop observing cancelFn := func() { d.ctx.UnregisterEventListener(listener.ID) d.ctx.UnregisterEventListener(domReadyListener.ID) d.elementObservers.Delete(observerId) d.ctx.SendEventToClient(ServerDOMStopObserveEvent, &ServerDOMStopObserveEventPayload{ ObserverId: observerId, }) } refetchFn := func() { d.ctx.SendEventToClient(ServerDOMObserveEvent, &ServerDOMObserveEventPayload{ Selector: selector, ObserverId: observerId, WithInnerHTML: options.WithInnerHTML, WithOuterHTML: options.WithOuterHTML, IdentifyChildren: options.IdentifyChildren, }) } d.ctx.registerOnCleanup(func() { cancelFn() }) return d.ctx.vm.ToValue([]interface{}{cancelFn, refetchFn}) } // jsCreateElement creates a new DOM element func (d *DOMManager) jsCreateElement(call goja.FunctionCall) goja.Value { tagName := call.Argument(0).String() // Create a promise that will be resolved with the created element promise, resolve, _ := d.ctx.vm.NewPromise() // Generate a unique request ID requestId := uuid.New().String() // Set up a one-time event listener for the response listener := d.ctx.RegisterEventListener(ClientDOMCreateResultEvent) listener.SetCallback(func(event *ClientPluginEvent) { var payload ClientDOMCreateResultEventPayload if event.ParsePayloadAs(ClientDOMCreateResultEvent, &payload) && payload.RequestID == requestId { if elemData, ok := payload.Element.(map[string]interface{}); ok { d.ctx.scheduler.ScheduleAsync(func() error { resolve(d.createDOMElementObject(elemData)) return nil }) } d.ctx.UnregisterEventListener(listener.ID) } }) // Send the create request to the client d.ctx.SendEventToClient(ServerDOMCreateEvent, &ServerDOMCreateEventPayload{ TagName: tagName, RequestID: requestId, }) return d.ctx.vm.ToValue(promise) } // jsAsElement returns a DOM element from an element ID // This is useful because we don't need to query the DOM for an element // We can just use the element ID that we already have to send events to the element func (d *DOMManager) jsAsElement(call goja.FunctionCall) goja.Value { elementId := call.Argument(0).String() element := d.ctx.vm.NewObject() _ = element.Set("id", elementId) // Assign methods to the element d.assignDOMElementMethods(element, elementId) return d.ctx.vm.ToValue(element) } // HandleObserverUpdate processes DOM observer updates from client func (d *DOMManager) HandleObserverUpdate(observerId string, elements []interface{}) { } // HandleDOMEvent processes DOM events from client func (d *DOMManager) HandleDOMEvent(elementId string, eventType string, eventData map[string]interface{}) { // Find all event listeners for this element and event type d.eventListeners.Range(func(key string, listener *DOMEventListener) bool { if listener.ElementId == elementId && listener.EventType == eventType { // Schedule callback execution in the VM d.ctx.scheduler.ScheduleAsync(func() error { _, err := listener.Callback(goja.Undefined(), d.ctx.vm.ToValue(eventData)) if err != nil { d.ctx.handleException(err) } return nil }) } return true }) } // createDOMElementObject creates a JavaScript object representing a DOM element func (d *DOMManager) createDOMElementObject(elemData map[string]interface{}) *goja.Object { elementObj := d.ctx.vm.NewObject() // Set basic properties elementId, _ := elemData["id"].(string) _ = elementObj.Set("id", elementId) if tagName, ok := elemData["tagName"].(string); ok { _ = elementObj.Set("tagName", tagName) } if text, ok := elemData["text"].(string); ok { _ = elementObj.Set("text", text) } if attributes, ok := elemData["attributes"].(map[string]interface{}); ok { attributesObj := d.ctx.vm.NewObject() for key, value := range attributes { _ = attributesObj.Set(key, value) } _ = elementObj.Set("attributes", attributesObj) } if style, ok := elemData["style"].(map[string]interface{}); ok { styleObj := d.ctx.vm.NewObject() for key, value := range style { _ = styleObj.Set(key, value) } _ = styleObj.Set("style", styleObj) } if className, ok := elemData["className"].(string); ok { _ = elementObj.Set("className", className) } if classList, ok := elemData["classList"].([]string); ok { _ = elementObj.Set("classList", classList) } if children, ok := elemData["children"].([]interface{}); ok { childrenObjs := make([]*goja.Object, 0, len(children)) for _, child := range children { if childData, ok := child.(map[string]interface{}); ok { childrenObjs = append(childrenObjs, d.createDOMElementObject(childData)) } } _ = elementObj.Set("children", childrenObjs) } if parent, ok := elemData["parent"].(map[string]interface{}); ok { elementObj.Set("parent", d.createDOMElementObject(parent)) } if innerHTML, ok := elemData["innerHTML"].(string); ok { _ = elementObj.Set("innerHTML", innerHTML) } if outerHTML, ok := elemData["outerHTML"].(string); ok { _ = elementObj.Set("outerHTML", outerHTML) } d.assignDOMElementMethods(elementObj, elementId) return elementObj } func (d *DOMManager) assignDOMElementMethods(elementObj *goja.Object, elementId string) { // Define methods _ = elementObj.Set("getText", func() goja.Value { return d.getElementText(elementId) }) _ = elementObj.Set("setText", func(text string) { d.setElementText(elementId, text) }) _ = elementObj.Set("setInnerHTML", func(innerHTML string) { d.setElementInnerHTML(elementId, innerHTML) }) _ = elementObj.Set("setOuterHTML", func(outerHTML string) { d.setElementOuterHTML(elementId, outerHTML) }) _ = elementObj.Set("getAttribute", func(name string) goja.Value { return d.getElementAttribute(elementId, name) }) _ = elementObj.Set("getAttributes", func() goja.Value { return d.getElementAttributes(elementId) }) _ = elementObj.Set("setAttribute", func(name, value string) { d.setElementAttribute(elementId, name, value) }) _ = elementObj.Set("removeAttribute", func(name string) { d.removeElementAttribute(elementId, name) }) _ = elementObj.Set("hasAttribute", func(name string) goja.Value { return d.hasElementAttribute(elementId, name) }) _ = elementObj.Set("getProperty", func(name string) goja.Value { return d.getElementProperty(elementId, name) }) _ = elementObj.Set("setProperty", func(name string, value interface{}) { d.setElementProperty(elementId, name, value) }) _ = elementObj.Set("addClass", func(call goja.FunctionCall) { classNames := make([]string, 0, len(call.Arguments)) for _, arg := range call.Arguments { if className := arg.String(); className != "" { classNames = append(classNames, className) } } d.addElementClass(elementId, classNames) }) _ = elementObj.Set("removeClass", func(call goja.FunctionCall) { classNames := make([]string, 0, len(call.Arguments)) for _, arg := range call.Arguments { if className := arg.String(); className != "" { classNames = append(classNames, className) } } d.removeElementClass(elementId, classNames) }) _ = elementObj.Set("hasClass", func(className string) goja.Value { return d.hasElementClass(elementId, className) }) _ = elementObj.Set("setStyle", func(property, value string) { d.setElementStyle(elementId, property, value) }) _ = elementObj.Set("setCssText", func(cssText string) { d.setElementCssText(elementId, cssText) }) _ = elementObj.Set("getStyle", func(call goja.FunctionCall) goja.Value { if len(call.Arguments) > 0 && !goja.IsUndefined(call.Argument(0)) { property := call.Argument(0).String() return d.ctx.vm.ToValue(d.getElementStyle(elementId, property)) } return d.ctx.vm.ToValue(d.getElementStyles(elementId)) }) _ = elementObj.Set("getComputedStyle", func(property string) goja.Value { return d.getElementComputedStyle(elementId, property) }) _ = elementObj.Set("append", func(child *goja.Object) { childId := child.Get("id").String() d.appendElement(elementId, childId) }) _ = elementObj.Set("before", func(sibling *goja.Object) { siblingId := sibling.Get("id").String() d.insertElementBefore(elementId, siblingId) }) _ = elementObj.Set("after", func(sibling *goja.Object) { siblingId := sibling.Get("id").String() d.insertElementAfter(elementId, siblingId) }) _ = elementObj.Set("remove", func() { d.removeElement(elementId) }) _ = elementObj.Set("getParent", func(opts QueryElementOptions) goja.Value { return d.getElementParent(elementId, opts) }) _ = elementObj.Set("getChildren", func(opts QueryElementOptions) goja.Value { return d.getElementChildren(elementId, opts) }) _ = elementObj.Set("addEventListener", func(event string, callback goja.Callable) func() { return d.addElementEventListener(elementId, event, callback) }) _ = elementObj.Set("getDataAttribute", func(key string) goja.Value { return d.getElementDataAttribute(elementId, key) }) _ = elementObj.Set("getDataAttributes", func() goja.Value { return d.getElementDataAttributes(elementId) }) _ = elementObj.Set("setDataAttribute", func(key, value string) { d.setElementDataAttribute(elementId, key, value) }) _ = elementObj.Set("removeDataAttribute", func(key string) { d.removeElementDataAttribute(elementId, key) }) _ = elementObj.Set("hasDataAttribute", func(key string) goja.Value { return d.hasElementDataAttribute(elementId, key) }) _ = elementObj.Set("hasStyle", func(property string) goja.Value { return d.hasElementStyle(elementId, property) }) _ = elementObj.Set("removeStyle", func(property string) { d.removeElementStyle(elementId, property) }) // Add element query methods _ = elementObj.Set("query", func(selector string, opts QueryElementOptions) goja.Value { return d.elementQuery(elementId, selector, opts) }) _ = elementObj.Set("queryOne", func(selector string, opts QueryElementOptions) goja.Value { return d.elementQueryOne(elementId, selector, opts) }) } // Element manipulation methods // These send events to the client and handle responses func (d *DOMManager) getElementText(elementId string) goja.Value { promise, resolve, _ := d.ctx.vm.NewPromise() // Generate a unique request ID requestId := uuid.New().String() // Listen for changes from the client listener := d.ctx.RegisterEventListener(ClientDOMElementUpdatedEvent) listener.SetCallback(func(event *ClientPluginEvent) { var payload ClientDOMElementUpdatedEventPayload if event.ParsePayloadAs(ClientDOMElementUpdatedEvent, &payload) { // Only process responses with matching element ID, action, and request ID if payload.Action == "getText" && payload.ElementId == elementId && payload.RequestID == requestId { if v, ok := payload.Result.(string); ok { d.ctx.scheduler.ScheduleAsync(func() error { resolve(d.ctx.vm.ToValue(v)) return nil }) } else { d.ctx.scheduler.ScheduleAsync(func() error { resolve(d.ctx.vm.ToValue("")) return nil }) } d.ctx.UnregisterEventListener(listener.ID) } } }) // Send the request to the client with the request ID d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "getText", Params: map[string]interface{}{}, RequestID: requestId, }) return d.ctx.vm.ToValue(promise) } func (d *DOMManager) setElementText(elementId, text string) { d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "setText", Params: map[string]interface{}{ "text": text, }, }) } func (d *DOMManager) getElementAttribute(elementId, name string) goja.Value { promise, resolve, _ := d.ctx.vm.NewPromise() // Generate a unique request ID requestId := uuid.New().String() // Listen for changes from the client listener := d.ctx.RegisterEventListener(ClientDOMElementUpdatedEvent) listener.SetCallback(func(event *ClientPluginEvent) { var payload ClientDOMElementUpdatedEventPayload if event.ParsePayloadAs(ClientDOMElementUpdatedEvent, &payload) { // Only process responses with matching element ID, action, and request ID if payload.Action == "getAttribute" && payload.ElementId == elementId && payload.RequestID == requestId { d.ctx.scheduler.ScheduleAsync(func() error { resolve(d.ctx.vm.ToValue(payload.Result)) return nil }) d.ctx.UnregisterEventListener(listener.ID) } } }) d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "getAttribute", Params: map[string]interface{}{ "name": name, }, RequestID: requestId, }) return d.ctx.vm.ToValue(promise) } func (d *DOMManager) setElementAttribute(elementId, name, value string) { d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "setAttribute", Params: map[string]interface{}{ "name": name, "value": value, }, }) } func (d *DOMManager) removeElementAttribute(elementId, name string) { d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "removeAttribute", Params: map[string]interface{}{ "name": name, }, }) } func (d *DOMManager) addElementClass(elementId string, classNames []string) { d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "addClass", Params: map[string]interface{}{ "classNames": classNames, }, }) } func (d *DOMManager) removeElementClass(elementId string, classNames []string) { d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "removeClass", Params: map[string]interface{}{ "classNames": classNames, }, }) } func (d *DOMManager) hasElementClass(elementId, className string) goja.Value { promise, resolve, _ := d.ctx.vm.NewPromise() // Generate a unique request ID requestId := uuid.New().String() // Listen for changes from the client listener := d.ctx.RegisterEventListener(ClientDOMElementUpdatedEvent) listener.SetCallback(func(event *ClientPluginEvent) { var payload ClientDOMElementUpdatedEventPayload if event.ParsePayloadAs(ClientDOMElementUpdatedEvent, &payload) { // Only process responses with matching element ID, action, and request ID if payload.Action == "hasClass" && payload.ElementId == elementId && payload.RequestID == requestId { if v, ok := payload.Result.(bool); ok { d.ctx.scheduler.ScheduleAsync(func() error { resolve(d.ctx.vm.ToValue(v)) return nil }) } else { d.ctx.scheduler.ScheduleAsync(func() error { resolve(d.ctx.vm.ToValue(false)) return nil }) } d.ctx.UnregisterEventListener(listener.ID) } } }) d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "hasClass", Params: map[string]interface{}{ "className": className, }, RequestID: requestId, }) return d.ctx.vm.ToValue(promise) } func (d *DOMManager) setElementStyle(elementId, property, value string) { d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "setStyle", Params: map[string]interface{}{ "property": property, "value": value, }, }) } func (d *DOMManager) setElementCssText(elementId, cssText string) { d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "setCssText", Params: map[string]interface{}{ "cssText": cssText, }, }) } func (d *DOMManager) getElementStyle(elementId, property string) goja.Value { promise, resolve, _ := d.ctx.vm.NewPromise() // Generate a unique request ID requestId := uuid.New().String() // Listen for changes from the client listener := d.ctx.RegisterEventListener(ClientDOMElementUpdatedEvent) listener.SetCallback(func(event *ClientPluginEvent) { var payload ClientDOMElementUpdatedEventPayload if event.ParsePayloadAs(ClientDOMElementUpdatedEvent, &payload) && payload.ElementId == elementId { if payload.Action == "getStyle" && payload.RequestID == requestId { if v, ok := payload.Result.(string); ok { d.ctx.scheduler.ScheduleAsync(func() error { resolve(d.ctx.vm.ToValue(v)) return nil }) } else { d.ctx.scheduler.ScheduleAsync(func() error { resolve(d.ctx.vm.ToValue("")) return nil }) } d.ctx.UnregisterEventListener(listener.ID) } } }) d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "getStyle", Params: map[string]interface{}{ "property": property, }, RequestID: requestId, }) return d.ctx.vm.ToValue(promise) } func (d *DOMManager) getElementComputedStyle(elementId, property string) goja.Value { promise, resolve, _ := d.ctx.vm.NewPromise() // Generate a unique request ID requestId := uuid.New().String() // Listen for changes from the client listener := d.ctx.RegisterEventListener(ClientDOMElementUpdatedEvent) listener.SetCallback(func(event *ClientPluginEvent) { var payload ClientDOMElementUpdatedEventPayload if event.ParsePayloadAs(ClientDOMElementUpdatedEvent, &payload) && payload.ElementId == elementId { if payload.Action == "getComputedStyle" && payload.RequestID == requestId { if v, ok := payload.Result.(string); ok { d.ctx.scheduler.ScheduleAsync(func() error { resolve(d.ctx.vm.ToValue(v)) return nil }) } else { d.ctx.scheduler.ScheduleAsync(func() error { resolve(d.ctx.vm.ToValue("")) return nil }) } d.ctx.UnregisterEventListener(listener.ID) } } }) d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "getComputedStyle", Params: map[string]interface{}{ "property": property, }, RequestID: requestId, }) return d.ctx.vm.ToValue(promise) } func (d *DOMManager) appendElement(parentID, childId string) { d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: parentID, Action: "append", Params: map[string]interface{}{ "childId": childId, }, }) } func (d *DOMManager) insertElementBefore(elementId, siblingId string) { d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "before", Params: map[string]interface{}{ "siblingId": siblingId, }, }) } func (d *DOMManager) insertElementAfter(elementId, siblingId string) { d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "after", Params: map[string]interface{}{ "siblingId": siblingId, }, }) } func (d *DOMManager) removeElement(elementId string) { d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "remove", Params: map[string]interface{}{}, }) } func (d *DOMManager) getElementParent(elementId string, opts QueryElementOptions) goja.Value { promise, resolve, _ := d.ctx.vm.NewPromise() // Generate a unique request ID requestId := uuid.New().String() // Listen for changes from the client listener := d.ctx.RegisterEventListener(ClientDOMElementUpdatedEvent) listener.SetCallback(func(event *ClientPluginEvent) { var payload ClientDOMElementUpdatedEventPayload if event.ParsePayloadAs(ClientDOMElementUpdatedEvent, &payload) { if payload.Action == "getParent" && payload.ElementId == elementId && payload.RequestID == requestId { if payload.Result != nil { if parentData, ok := payload.Result.(map[string]interface{}); ok { d.ctx.scheduler.ScheduleAsync(func() error { resolve(d.ctx.vm.ToValue(d.createDOMElementObject(parentData))) return nil }) } else { d.ctx.scheduler.ScheduleAsync(func() error { resolve(d.ctx.vm.ToValue(goja.Null())) return nil }) } } else { d.ctx.scheduler.ScheduleAsync(func() error { resolve(d.ctx.vm.ToValue(goja.Null())) return nil }) } d.ctx.UnregisterEventListener(listener.ID) } } }) d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "getParent", Params: map[string]interface{}{"opts": opts}, RequestID: requestId, }) return d.ctx.vm.ToValue(promise) } func (d *DOMManager) getElementChildren(elementId string, opts QueryElementOptions) goja.Value { promise, resolve, _ := d.ctx.vm.NewPromise() // Generate a unique request ID requestId := uuid.New().String() // Listen for changes from the client listener := d.ctx.RegisterEventListener(ClientDOMElementUpdatedEvent) listener.SetCallback(func(event *ClientPluginEvent) { var payload ClientDOMElementUpdatedEventPayload if event.ParsePayloadAs(ClientDOMElementUpdatedEvent, &payload) { if payload.Action == "getChildren" && payload.ElementId == elementId && payload.RequestID == requestId { if payload.Result != nil { if childrenData, ok := payload.Result.([]interface{}); ok { d.ctx.scheduler.ScheduleAsync(func() error { childrenObjs := make([]interface{}, 0, len(childrenData)) for _, child := range childrenData { if childData, ok := child.(map[string]interface{}); ok { childrenObjs = append(childrenObjs, d.createDOMElementObject(childData)) } } resolve(d.ctx.vm.ToValue(childrenObjs)) return nil }) } else { d.ctx.scheduler.ScheduleAsync(func() error { resolve(d.ctx.vm.ToValue([]interface{}{})) return nil }) } } else { d.ctx.scheduler.ScheduleAsync(func() error { resolve(d.ctx.vm.ToValue([]interface{}{})) return nil }) } d.ctx.UnregisterEventListener(listener.ID) } } }) d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "getChildren", Params: map[string]interface{}{"opts": opts}, RequestID: requestId, }) return d.ctx.vm.ToValue(promise) } func (d *DOMManager) addElementEventListener(elementId, event string, callback goja.Callable) func() { // Create a unique ID for this event listener listenerID := uuid.New().String() // Store the event listener listener := &DOMEventListener{ ID: listenerID, ElementId: elementId, EventType: event, Callback: callback, } d.eventListeners.Set(listenerID, listener) // Send the request to add the event listener to the client d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "addEventListener", Params: map[string]interface{}{ "event": event, "listenerID": listenerID, }, }) // Return a function to remove the event listener return func() { d.eventListeners.Delete(listenerID) // Send the request to remove the event listener from the client d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "removeEventListener", Params: map[string]interface{}{ "event": event, "listenerID": listenerID, }, }) } } func (d *DOMManager) getElementAttributes(elementId string) goja.Value { promise, resolve, _ := d.ctx.vm.NewPromise() // Generate a unique request ID requestId := uuid.New().String() // Listen for changes from the client listener := d.ctx.RegisterEventListener(ClientDOMElementUpdatedEvent) listener.SetCallback(func(event *ClientPluginEvent) { var payload ClientDOMElementUpdatedEventPayload if event.ParsePayloadAs(ClientDOMElementUpdatedEvent, &payload) { if payload.Action == "getAttributes" && payload.ElementId == elementId && payload.RequestID == requestId { d.ctx.scheduler.ScheduleAsync(func() error { resolve(d.ctx.vm.ToValue(payload.Result)) return nil }) d.ctx.UnregisterEventListener(listener.ID) } } }) d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "getAttributes", Params: map[string]interface{}{}, RequestID: requestId, }) return d.ctx.vm.ToValue(promise) } func (d *DOMManager) hasElementAttribute(elementId, name string) goja.Value { promise, resolve, _ := d.ctx.vm.NewPromise() // Generate a unique request ID requestId := uuid.New().String() // Listen for changes from the client listener := d.ctx.RegisterEventListener(ClientDOMElementUpdatedEvent) listener.SetCallback(func(event *ClientPluginEvent) { var payload ClientDOMElementUpdatedEventPayload if event.ParsePayloadAs(ClientDOMElementUpdatedEvent, &payload) { if payload.Action == "hasAttribute" && payload.ElementId == elementId && payload.RequestID == requestId { if v, ok := payload.Result.(bool); ok { d.ctx.scheduler.ScheduleAsync(func() error { resolve(d.ctx.vm.ToValue(v)) return nil }) } else { d.ctx.scheduler.ScheduleAsync(func() error { resolve(d.ctx.vm.ToValue(false)) return nil }) } d.ctx.UnregisterEventListener(listener.ID) } } }) d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "hasAttribute", Params: map[string]interface{}{ "name": name, }, RequestID: requestId, }) return d.ctx.vm.ToValue(promise) } func (d *DOMManager) getElementProperty(elementId, name string) goja.Value { promise, resolve, _ := d.ctx.vm.NewPromise() // Generate a unique request ID requestId := uuid.New().String() // Listen for changes from the client listener := d.ctx.RegisterEventListener(ClientDOMElementUpdatedEvent) listener.SetCallback(func(event *ClientPluginEvent) { var payload ClientDOMElementUpdatedEventPayload if event.ParsePayloadAs(ClientDOMElementUpdatedEvent, &payload) { if payload.Action == "getProperty" && payload.ElementId == elementId && payload.RequestID == requestId { d.ctx.scheduler.ScheduleAsync(func() error { resolve(d.ctx.vm.ToValue(payload.Result)) return nil }) d.ctx.UnregisterEventListener(listener.ID) } } }) d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "getProperty", Params: map[string]interface{}{ "name": name, }, RequestID: requestId, }) return d.ctx.vm.ToValue(promise) } func (d *DOMManager) setElementProperty(elementId, name string, value interface{}) { d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "setProperty", Params: map[string]interface{}{ "name": name, "value": value, }, }) } func (d *DOMManager) getElementStyles(elementId string) goja.Value { promise, resolve, _ := d.ctx.vm.NewPromise() // Generate a unique request ID requestId := uuid.New().String() // Listen for changes from the client listener := d.ctx.RegisterEventListener(ClientDOMElementUpdatedEvent) listener.SetCallback(func(event *ClientPluginEvent) { var payload ClientDOMElementUpdatedEventPayload if event.ParsePayloadAs(ClientDOMElementUpdatedEvent, &payload) { if payload.Action == "getStyle" && payload.ElementId == elementId && payload.RequestID == requestId && payload.Result != nil { d.ctx.scheduler.ScheduleAsync(func() error { resolve(d.ctx.vm.ToValue(payload.Result)) return nil }) d.ctx.UnregisterEventListener(listener.ID) } } }) d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "getStyle", Params: map[string]interface{}{}, RequestID: requestId, }) return d.ctx.vm.ToValue(promise) } func (d *DOMManager) hasElementStyle(elementId, property string) goja.Value { promise, resolve, _ := d.ctx.vm.NewPromise() // Generate a unique request ID requestId := uuid.New().String() // Listen for changes from the client listener := d.ctx.RegisterEventListener(ClientDOMElementUpdatedEvent) listener.SetCallback(func(event *ClientPluginEvent) { var payload ClientDOMElementUpdatedEventPayload if event.ParsePayloadAs(ClientDOMElementUpdatedEvent, &payload) { if payload.Action == "hasStyle" && payload.ElementId == elementId && payload.RequestID == requestId { if v, ok := payload.Result.(bool); ok { d.ctx.scheduler.ScheduleAsync(func() error { resolve(d.ctx.vm.ToValue(v)) return nil }) } else { d.ctx.scheduler.ScheduleAsync(func() error { resolve(d.ctx.vm.ToValue(false)) return nil }) } d.ctx.UnregisterEventListener(listener.ID) } } }) d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "hasStyle", Params: map[string]interface{}{ "property": property, }, RequestID: requestId, }) return d.ctx.vm.ToValue(promise) } func (d *DOMManager) getElementDataAttribute(elementId, key string) goja.Value { promise, resolve, _ := d.ctx.vm.NewPromise() // Generate a unique request ID requestId := uuid.New().String() // Listen for changes from the client listener := d.ctx.RegisterEventListener(ClientDOMElementUpdatedEvent) listener.SetCallback(func(event *ClientPluginEvent) { var payload ClientDOMElementUpdatedEventPayload if event.ParsePayloadAs(ClientDOMElementUpdatedEvent, &payload) { if payload.Action == "getDataAttribute" && payload.ElementId == elementId && payload.RequestID == requestId { d.ctx.scheduler.ScheduleAsync(func() error { resolve(d.ctx.vm.ToValue(payload.Result)) return nil }) d.ctx.UnregisterEventListener(listener.ID) } } }) d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "getDataAttribute", Params: map[string]interface{}{ "key": key, }, RequestID: requestId, }) return d.ctx.vm.ToValue(promise) } func (d *DOMManager) getElementDataAttributes(elementId string) goja.Value { promise, resolve, _ := d.ctx.vm.NewPromise() // Generate a unique request ID requestId := uuid.New().String() // Listen for changes from the client listener := d.ctx.RegisterEventListener(ClientDOMElementUpdatedEvent) listener.SetCallback(func(event *ClientPluginEvent) { var payload ClientDOMElementUpdatedEventPayload if event.ParsePayloadAs(ClientDOMElementUpdatedEvent, &payload) { if payload.Action == "getDataAttributes" && payload.ElementId == elementId && payload.RequestID == requestId { d.ctx.scheduler.ScheduleAsync(func() error { resolve(d.ctx.vm.ToValue(payload.Result)) return nil }) d.ctx.UnregisterEventListener(listener.ID) } } }) d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "getDataAttributes", Params: map[string]interface{}{}, RequestID: requestId, }) return d.ctx.vm.ToValue(promise) } func (d *DOMManager) setElementDataAttribute(elementId, key, value string) { d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "setDataAttribute", Params: map[string]interface{}{ "key": key, "value": value, }, }) } func (d *DOMManager) removeElementDataAttribute(elementId, key string) { d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "removeDataAttribute", Params: map[string]interface{}{ "key": key, }, }) } func (d *DOMManager) hasElementDataAttribute(elementId, key string) goja.Value { promise, resolve, _ := d.ctx.vm.NewPromise() // Generate a unique request ID requestId := uuid.New().String() // Listen for changes from the client listener := d.ctx.RegisterEventListener(ClientDOMElementUpdatedEvent) listener.SetCallback(func(event *ClientPluginEvent) { var payload ClientDOMElementUpdatedEventPayload if event.ParsePayloadAs(ClientDOMElementUpdatedEvent, &payload) { if payload.Action == "hasDataAttribute" && payload.ElementId == elementId && payload.RequestID == requestId { if v, ok := payload.Result.(bool); ok { d.ctx.scheduler.ScheduleAsync(func() error { resolve(d.ctx.vm.ToValue(v)) return nil }) } else { d.ctx.scheduler.ScheduleAsync(func() error { resolve(d.ctx.vm.ToValue(false)) return nil }) } d.ctx.UnregisterEventListener(listener.ID) } } }) d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "hasDataAttribute", Params: map[string]interface{}{ "key": key, }, RequestID: requestId, }) return d.ctx.vm.ToValue(promise) } func (d *DOMManager) removeElementStyle(elementId, property string) { d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "removeStyle", Params: map[string]interface{}{ "property": property, }, }) } // elementQuery handles querying for multiple DOM elements from a parent element func (d *DOMManager) elementQuery(elementId, selector string, opts QueryElementOptions) goja.Value { promise, resolve, _ := d.ctx.vm.NewPromise() // Generate a unique request ID requestId := uuid.New().String() // Set up a one-time event listener for the response listener := d.ctx.RegisterEventListener(ClientDOMQueryResultEvent) listener.SetCallback(func(event *ClientPluginEvent) { var payload ClientDOMQueryResultEventPayload if event.ParsePayloadAs(ClientDOMQueryResultEvent, &payload) && payload.RequestID == requestId { d.ctx.scheduler.ScheduleAsync(func() error { elemObjs := make([]interface{}, 0, len(payload.Elements)) for _, elem := range payload.Elements { if elemData, ok := elem.(map[string]interface{}); ok { elemObjs = append(elemObjs, d.createDOMElementObject(elemData)) } } resolve(d.ctx.vm.ToValue(elemObjs)) return nil }) d.ctx.UnregisterEventListener(listener.ID) } }) // Send the query request to the client d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "query", Params: map[string]interface{}{ "selector": selector, "requestId": requestId, "withInnerHTML": opts.WithInnerHTML, "withOuterHTML": opts.WithOuterHTML, }, }) return d.ctx.vm.ToValue(promise) } // elementQueryOne handles querying for a single DOM element from a parent element func (d *DOMManager) elementQueryOne(elementId, selector string, opts QueryElementOptions) goja.Value { promise, resolve, _ := d.ctx.vm.NewPromise() // Generate a unique request ID requestId := uuid.New().String() // Set up a one-time event listener for the response listener := d.ctx.RegisterEventListener(ClientDOMQueryOneResultEvent) listener.SetCallback(func(event *ClientPluginEvent) { var payload ClientDOMQueryOneResultEventPayload if event.ParsePayloadAs(ClientDOMQueryOneResultEvent, &payload) && payload.RequestID == requestId { d.ctx.scheduler.ScheduleAsync(func() error { if payload.Element != nil { if elemData, ok := payload.Element.(map[string]interface{}); ok { resolve(d.ctx.vm.ToValue(d.createDOMElementObject(elemData))) } else { resolve(d.ctx.vm.ToValue(goja.Null())) } } else { resolve(d.ctx.vm.ToValue(goja.Null())) } return nil }) d.ctx.UnregisterEventListener(listener.ID) } }) // Send the query request to the client d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "queryOne", Params: map[string]interface{}{ "selector": selector, "requestId": requestId, "withInnerHTML": opts.WithInnerHTML, "withOuterHTML": opts.WithOuterHTML, }, }) return d.ctx.vm.ToValue(promise) } func (d *DOMManager) setElementInnerHTML(elementId, innerHTML string) { d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "setInnerHTML", Params: map[string]interface{}{"innerHTML": innerHTML}, }) } func (d *DOMManager) setElementOuterHTML(elementId, outerHTML string) { d.ctx.SendEventToClient(ServerDOMManipulateEvent, &ServerDOMManipulateEventPayload{ ElementId: elementId, Action: "setOuterHTML", Params: map[string]interface{}{"outerHTML": outerHTML}, }) } // jsObserveInView starts observing DOM elements matching a selector when they are in the viewport func (d *DOMManager) jsObserveInView(call goja.FunctionCall) goja.Value { selector := call.Argument(0).String() callback, ok := goja.AssertFunction(call.Argument(1)) if !ok { d.ctx.handleTypeError("observeInView requires a callback function") } options := d.getQueryElementOptions(call.Argument(2)) // Get margin settings if provided margin := "0px" optsObj := call.Argument(2).ToObject(d.ctx.vm) marginVal := optsObj.Get("margin") if marginVal != nil && !goja.IsUndefined(marginVal) && !goja.IsNull(marginVal) { margin = marginVal.String() } // Create observer ID observerId := uuid.New().String() // Store the observer observer := &ElementObserver{ ID: observerId, Selector: selector, Callback: callback, } d.elementObservers.Set(observerId, observer) // Send observe request to client d.ctx.SendEventToClient(ServerDOMObserveInViewEvent, &ServerDOMObserveInViewEventPayload{ Selector: selector, ObserverId: observerId, WithInnerHTML: options.WithInnerHTML, WithOuterHTML: options.WithOuterHTML, IdentifyChildren: options.IdentifyChildren, Margin: margin, }) // Start a goroutine to handle observer updates listener := d.ctx.RegisterEventListener(ClientDOMObserveResultEvent) listener.SetCallback(func(event *ClientPluginEvent) { var payload ClientDOMObserveResultEventPayload if event.ParsePayloadAs(ClientDOMObserveResultEvent, &payload) && payload.ObserverId == observerId { d.ctx.scheduler.ScheduleAsync(func() error { observer, exists := d.elementObservers.Get(observerId) if !exists { return nil } // Convert elements to DOM element objects directly in the VM thread elemObjs := make([]interface{}, 0, len(payload.Elements)) for _, elem := range payload.Elements { if elemData, ok := elem.(map[string]interface{}); ok { elemObjs = append(elemObjs, d.createDOMElementObject(elemData)) } } // Call the callback directly now that we have all elements _, err := observer.Callback(goja.Undefined(), d.ctx.vm.ToValue(elemObjs)) if err != nil { d.ctx.handleException(err) } return nil }) } }) // Listen for DOM ready events to re-observe elements after page reload domReadyListener := d.ctx.RegisterEventListener(ClientDOMReadyEvent) domReadyListener.SetCallback(func(event *ClientPluginEvent) { // Re-send the observe request when the DOM is ready d.ctx.SendEventToClient(ServerDOMObserveInViewEvent, &ServerDOMObserveInViewEventPayload{ Selector: selector, ObserverId: observerId, WithInnerHTML: options.WithInnerHTML, WithOuterHTML: options.WithOuterHTML, IdentifyChildren: options.IdentifyChildren, Margin: margin, }) }) // Return a function to stop observing cancelFn := func() { d.ctx.UnregisterEventListener(listener.ID) d.ctx.UnregisterEventListener(domReadyListener.ID) d.elementObservers.Delete(observerId) d.ctx.SendEventToClient(ServerDOMStopObserveEvent, &ServerDOMStopObserveEventPayload{ ObserverId: observerId, }) } refetchFn := func() { d.ctx.SendEventToClient(ServerDOMObserveInViewEvent, &ServerDOMObserveInViewEventPayload{ Selector: selector, ObserverId: observerId, WithInnerHTML: options.WithInnerHTML, WithOuterHTML: options.WithOuterHTML, IdentifyChildren: options.IdentifyChildren, Margin: margin, }) } d.ctx.registerOnCleanup(func() { cancelFn() }) return d.ctx.vm.ToValue([]interface{}{cancelFn, refetchFn}) }