1611 lines
48 KiB
Go
1611 lines
48 KiB
Go
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})
|
|
}
|