diff --git a/Runtime/ChromeDevtools/BrowserTab.cs b/Runtime/ChromeDevtools/BrowserTab.cs index 0f4e23f92575d37e712e05287aee5b0c0171114f..4daa6b1c4c86cec896be7341c8b0a55304991511 100644 --- a/Runtime/ChromeDevtools/BrowserTab.cs +++ b/Runtime/ChromeDevtools/BrowserTab.cs @@ -30,14 +30,14 @@ namespace bessw.Unity.WebView.ChromeDevTools this.devtools = new DevtoolsProtocolHandler(new DevtoolsWebsocket(pageTarget.WebSocketDebuggerUrl)); // register DOM event handlers - this.devtools.DOM_AttributeModifiedEventHandler += (attributeModifiedEvent) => DomNodeWrapper.onAttributeModified(this, attributeModifiedEvent); - this.devtools.DOM_AttributeRemovedEventHandler += (attributeRemovedEvent) => DomNodeWrapper.onAttributeRemoved(this, attributeRemovedEvent); - this.devtools.DOM_CharacterDataModifiedEventHandler += (characterDataModifiedEvent) => DomNodeWrapper.onCharacterDataModified(this, characterDataModifiedEvent); - this.devtools.DOM_ChildNodeCountUpdatedEventHandler += (childNodeCountUpdatedEvent) => DomNodeWrapper.onChildNodeCountUpdated(this, childNodeCountUpdatedEvent); - this.devtools.DOM_ChildNodeInsertedEventHandler += (childNodeInsertedEvent) => DomNodeWrapper.onChildNodeInserted(this, childNodeInsertedEvent); - this.devtools.DOM_ChildNodeRemovedEventHandler += (childNodeRemovedEvent) => DomNodeWrapper.onChildNodeRemoved(this, childNodeRemovedEvent); - this.devtools.DOM_DocumentUpdatedEventHandler += (documentUpdatedEvent) => DomNodeWrapper.onDocumentUpdated(this, documentUpdatedEvent); - this.devtools.DOM_SetChildNodesEventHandler += (setChildNodesEvent) => DomNodeWrapper.onSetChildNodes(this, setChildNodesEvent); + this.devtools.onDomAttributeModified += (attributeModifiedEvent) => DomNodeWrapper.AttributeModifiedEventHandler(this, attributeModifiedEvent); + this.devtools.onDomAttributeRemoved += (attributeRemovedEvent) => DomNodeWrapper.AttributeRemovedEventHandler(this, attributeRemovedEvent); + this.devtools.onDomCharacterDataModified += (characterDataModifiedEvent) => DomNodeWrapper.CharacterDataModifiedEventHandler(this, characterDataModifiedEvent); + this.devtools.onDomChildNodeCountUpdated += (childNodeCountUpdatedEvent) => DomNodeWrapper.ChildNodeCountUpdatedEventHandler(this, childNodeCountUpdatedEvent); + this.devtools.onDomChildNodeInserted += (childNodeInsertedEvent) => DomNodeWrapper.ChildNodeInsertedEventHandler(this, childNodeInsertedEvent); + this.devtools.onDomChildNodeRemoved += (childNodeRemovedEvent) => DomNodeWrapper.ChildNodeRemovedEventHandler(this, childNodeRemovedEvent); + this.devtools.onDomDocumentUpdated += (documentUpdatedEvent) => DomNodeWrapper.DocumentUpdatedEventHandler(this, documentUpdatedEvent); + this.devtools.onDomSetChildNodes += (setChildNodesEvent) => DomNodeWrapper.SetChildNodesEventHandler(this, setChildNodesEvent); } public IEnumerator Update() @@ -73,7 +73,7 @@ namespace bessw.Unity.WebView.ChromeDevTools { // register screencast frame event handler // TODO: deregister on stop stream - devtools.screencastFrameEventHandler += (screencastFrameEvent frameEvent) => + devtools.onScreencastFrame += (screencastFrameEvent frameEvent) => { // send an ack for this frame var frameAck = new screencastFrameAck diff --git a/Runtime/ChromeDevtools/DevtoolsProtocolHandler.cs b/Runtime/ChromeDevtools/DevtoolsProtocolHandler.cs index 737ffe341b6bcb820180248fe7d930099315dead..97e6146f4591b533080071a29c28782647b4c4b6 100644 --- a/Runtime/ChromeDevtools/DevtoolsProtocolHandler.cs +++ b/Runtime/ChromeDevtools/DevtoolsProtocolHandler.cs @@ -23,18 +23,18 @@ namespace bessw.Unity.WebView.ChromeDevTools private ConcurrentDictionary<long, ResponseTypeAndCallback> commandResponseDict = new ConcurrentDictionary<long, ResponseTypeAndCallback>(); // devtools events - public event Action<screencastFrameEvent> screencastFrameEventHandler; - public event Action<screencastVisibilityChangedEvent> screencastVisibilityChangedEventHandler; + public event Action<screencastFrameEvent> onScreencastFrame; + public event Action<screencastVisibilityChangedEvent> onScreencastVisibilityChanged; #region DOM events - public event Action<attributeModifiedEvent> DOM_AttributeModifiedEventHandler; - public event Action<attributeRemovedEvent> DOM_AttributeRemovedEventHandler; - public event Action<characterDataModifiedEvent> DOM_CharacterDataModifiedEventHandler; - public event Action<childNodeCountUpdatedEvent> DOM_ChildNodeCountUpdatedEventHandler; - public event Action<childNodeInsertedEvent> DOM_ChildNodeInsertedEventHandler; - public event Action<childNodeRemovedEvent> DOM_ChildNodeRemovedEventHandler; - public event Action<documentUpdatedEvent> DOM_DocumentUpdatedEventHandler; - public event Action<setChildNodesEvent> DOM_SetChildNodesEventHandler; + public event Action<attributeModifiedEvent> onDomAttributeModified; + public event Action<attributeRemovedEvent> onDomAttributeRemoved; + public event Action<characterDataModifiedEvent> onDomCharacterDataModified; + public event Action<childNodeCountUpdatedEvent> onDomChildNodeCountUpdated; + public event Action<childNodeInsertedEvent> onDomChildNodeInserted; + public event Action<childNodeRemovedEvent> onDomChildNodeRemoved; + public event Action<documentUpdatedEvent> onDomDocumentUpdated; + public event Action<setChildNodesEvent> onDomSetChildNodes; #endregion DOM events public event Action<DevtoolsEventWrapper> unknownEventHandler; @@ -122,37 +122,37 @@ namespace bessw.Unity.WebView.ChromeDevTools switch (ev.Method) { case "Page.screencastFrame": - screencastFrameEventHandler?.Invoke( ev.Params.ToObject<screencastFrameEvent>(Browser.devtoolsSerializer) ); + onScreencastFrame?.Invoke( ev.Params.ToObject<screencastFrameEvent>(Browser.devtoolsSerializer) ); break; case "Page.screencastVisibilityChanged": - screencastVisibilityChangedEventHandler?.Invoke( ev.Params.ToObject<screencastVisibilityChangedEvent>(Browser.devtoolsSerializer) ); + onScreencastVisibilityChanged?.Invoke( ev.Params.ToObject<screencastVisibilityChangedEvent>(Browser.devtoolsSerializer) ); break; // switch cases that invoke the event handlers for the DOM events #region DOM events case "DOM.attributeModified": - DOM_AttributeModifiedEventHandler?.Invoke( ev.Params.ToObject<attributeModifiedEvent>(Browser.devtoolsSerializer) ); + onDomAttributeModified?.Invoke( ev.Params.ToObject<attributeModifiedEvent>(Browser.devtoolsSerializer) ); break; case "DOM.attributeRemoved": - DOM_AttributeRemovedEventHandler?.Invoke( ev.Params.ToObject<attributeRemovedEvent>(Browser.devtoolsSerializer) ); + onDomAttributeRemoved?.Invoke( ev.Params.ToObject<attributeRemovedEvent>(Browser.devtoolsSerializer) ); break; case "DOM.characterDataModified": - DOM_CharacterDataModifiedEventHandler?.Invoke( ev.Params.ToObject<characterDataModifiedEvent>(Browser.devtoolsSerializer) ); + onDomCharacterDataModified?.Invoke( ev.Params.ToObject<characterDataModifiedEvent>(Browser.devtoolsSerializer) ); break; case "DOM.childNodeCountUpdated": - DOM_ChildNodeCountUpdatedEventHandler?.Invoke( ev.Params.ToObject<childNodeCountUpdatedEvent>(Browser.devtoolsSerializer) ); + onDomChildNodeCountUpdated?.Invoke( ev.Params.ToObject<childNodeCountUpdatedEvent>(Browser.devtoolsSerializer) ); break; case "DOM.childNodeInserted": - DOM_ChildNodeInsertedEventHandler?.Invoke( ev.Params.ToObject<childNodeInsertedEvent>(Browser.devtoolsSerializer) ); + onDomChildNodeInserted?.Invoke( ev.Params.ToObject<childNodeInsertedEvent>(Browser.devtoolsSerializer) ); break; case "DOM.childNodeRemoved": - DOM_ChildNodeRemovedEventHandler?.Invoke( ev.Params.ToObject<childNodeRemovedEvent>(Browser.devtoolsSerializer) ); + onDomChildNodeRemoved?.Invoke( ev.Params.ToObject<childNodeRemovedEvent>(Browser.devtoolsSerializer) ); break; case "DOM.documentUpdated": - DOM_DocumentUpdatedEventHandler?.Invoke( ev.Params.ToObject<documentUpdatedEvent>(Browser.devtoolsSerializer) ); + onDomDocumentUpdated?.Invoke( ev.Params.ToObject<documentUpdatedEvent>(Browser.devtoolsSerializer) ); break; case "DOM.setChildNodes": - DOM_SetChildNodesEventHandler?.Invoke( ev.Params.ToObject<setChildNodesEvent>(Browser.devtoolsSerializer) ); + onDomSetChildNodes?.Invoke( ev.Params.ToObject<setChildNodesEvent>(Browser.devtoolsSerializer) ); break; #endregion DOM events diff --git a/Runtime/ChromeDevtools/DomNodeWrapper.cs b/Runtime/ChromeDevtools/DomNodeWrapper.cs index c59666ea08cb811f13befbc3de192eabed42b2ba..4d6c163fa802dff2a40462c734cfb97967264437 100644 --- a/Runtime/ChromeDevtools/DomNodeWrapper.cs +++ b/Runtime/ChromeDevtools/DomNodeWrapper.cs @@ -34,6 +34,14 @@ namespace bessw.Unity.WebView.ChromeDevTools /// <remarks>May be null if it has not jet been requested from the browser</remarks> public Node Node { get; protected set; } + public event Action<attributeModifiedEvent>? OnAttributeModified; + public event Action<attributeRemovedEvent>? OnDomAttributeRemoved; + public event Action<characterDataModifiedEvent>? OnDomCharacterDataModified; + public event Action<childNodeCountUpdatedEvent>? OnDomChildNodeCountUpdated; + public event Action<childNodeInsertedEvent>? OnDomChildNodeInserted; + public event Action<childNodeRemovedEvent>? OnDomChildNodeRemoved; + public event Action<setChildNodesEvent>? OnDomSetChildNodes; + /// <summary> /// Reference to the browser tab that this dom node belongs to @@ -75,63 +83,68 @@ namespace bessw.Unity.WebView.ChromeDevTools #region Event Handlers - public static void onAttributeModified(BrowserTab tab, attributeModifiedEvent eventData) + public static void AttributeModifiedEventHandler(BrowserTab tab, attributeModifiedEvent eventData) { var domNode = createOrUpdateNode(tab, eventData.nodeId); if (domNode.Node != null) { domNode.Node.attributes[eventData.name] = eventData.value; + domNode.OnAttributeModified?.Invoke(eventData); } else { throw new InvalidOperationException("AttributeModified event was fired, the node info has not yet been recieved"); } } - public static void onAttributeRemoved(BrowserTab tab, attributeRemovedEvent eventData) + public static void AttributeRemovedEventHandler(BrowserTab tab, attributeRemovedEvent eventData) { var domNode = createOrUpdateNode(tab, eventData.nodeId); if (domNode.Node != null) { domNode.Node.attributes.Remove(eventData.name); + domNode.OnDomAttributeRemoved?.Invoke(eventData); } else { throw new InvalidOperationException("AttributeRemoved event was fired, the node info has not yet been recieved"); } } - public static void onCharacterDataModified(BrowserTab tab, characterDataModifiedEvent eventData) + public static void CharacterDataModifiedEventHandler(BrowserTab tab, characterDataModifiedEvent eventData) { var domNode = createOrUpdateNode(tab, eventData.nodeId); if (domNode.Node != null) { domNode.Node.nodeValue = eventData.characterData; + domNode.OnDomCharacterDataModified?.Invoke(eventData); } else { throw new InvalidOperationException("CharacterDataModified event was fired, the node info has not yet been recieved"); } } - public static void onChildNodeCountUpdated(BrowserTab tab, childNodeCountUpdatedEvent eventData) + public static void ChildNodeCountUpdatedEventHandler(BrowserTab tab, childNodeCountUpdatedEvent eventData) { var domNode = createOrUpdateNode(tab, eventData.nodeId); if (domNode.Node != null) { domNode.Node.childNodeCount = eventData.childNodeCount; + domNode.OnDomChildNodeCountUpdated?.Invoke(eventData); } else { throw new InvalidOperationException("ChildNodeCountUpdated event was fired, the node info has not yet been recieved"); } } - public static void onChildNodeInserted(BrowserTab tab, childNodeInsertedEvent eventData) + public static void ChildNodeInsertedEventHandler(BrowserTab tab, childNodeInsertedEvent eventData) { var parentNode = createOrUpdateNode(tab, eventData.parentNodeId); var childNode = createOrUpdateNode(tab, eventData.node.nodeId, eventData.parentNodeId, eventData.node.backendNodeId, eventData.node); if (parentNode.Node != null && childNode.Node != null) { parentNode.Node.children.Insert(eventData.previousNodeId, childNode.Node); + parentNode.OnDomChildNodeInserted?.Invoke(eventData); } else { throw new InvalidOperationException("ChildNodeInserted event was fired, the node info has not yet been recieved"); } } - public static void onChildNodeRemoved(BrowserTab tab, childNodeRemovedEvent eventData) + public static void ChildNodeRemovedEventHandler(BrowserTab tab, childNodeRemovedEvent eventData) { if ( tab.domNodes.TryGetValue(eventData.parentNodeId, out var parentNode) && @@ -140,18 +153,19 @@ namespace bessw.Unity.WebView.ChromeDevTools childNode.Node != null) { parentNode.Node.children.Remove(childNode.Node); + parentNode.OnDomChildNodeRemoved?.Invoke(eventData); } else { //throw new InvalidOperationException("ChildNodeRemoved event was fired, the node info has not yet been recieved"); } } - public static void onDocumentUpdated(BrowserTab tab, documentUpdatedEvent eventData) + public static void DocumentUpdatedEventHandler(BrowserTab tab, documentUpdatedEvent eventData) { // clear the domNodes dictionary tab.domNodes.Clear(); } - public static void onSetChildNodes(BrowserTab tab, setChildNodesEvent eventData) + public static void SetChildNodesEventHandler(BrowserTab tab, setChildNodesEvent eventData) { var parentNode = createOrUpdateNode(tab, eventData.parentId); if (parentNode.Node != null) @@ -161,6 +175,7 @@ namespace bessw.Unity.WebView.ChromeDevTools createOrUpdateNode(tab, node.nodeId, eventData.parentId, node.backendNodeId, node); } parentNode.Node.children = new(eventData.nodes); + parentNode.OnDomSetChildNodes?.Invoke(eventData); } else { throw new InvalidOperationException("SetChildNodes event was fired, the node info has not yet been recieved"); } @@ -218,7 +233,137 @@ namespace bessw.Unity.WebView.ChromeDevTools return createOrUpdateNode(tab, commandResponse.nodeId); } + public static async Task<DomNodeWrapper> getNodeForLocationAsync(BrowserTab tab, int x, int y, bool? includeUserAgentShadowDOM = null, bool? ignorePointerEventsNone = null) + { + var commandResponse = await tab.devtools.SendCommandAsync<getNodeForLocation, getNodeForLocationCommandResponse>(new getNodeForLocation + { + x = x, + y = y, + includeUserAgentShadowDOM = includeUserAgentShadowDOM, + ignorePointerEventsNone = ignorePointerEventsNone + }); + if (commandResponse.nodeId == null) throw new InvalidOperationException("nodeId was not provided, is the dom domain enabled?"); + + return createOrUpdateNode(tab, (int)commandResponse.nodeId, backendNodeId: commandResponse.backendNodeId); + } + + /// <summary> + /// Enables DOM agent for the given page. + /// </summary> + /// <param name="tab"></param> + /// <param name="includeWhitespace"></param> + /// <returns></returns> + public static Task enableAsync(BrowserTab tab, string? includeWhitespace = null) + { + return tab.devtools.SendCommandAsync(new enable + { + includeWhitespace = includeWhitespace + }); + } + + /// <summary> + /// Disables DOM agent for the given page. + /// </summary> + /// <param name="tab"></param> + /// <returns></returns> + public static Task disableAsync(BrowserTab tab) + { + return tab.devtools.SendCommandAsync(new disable()); + } + + /// <summary> + /// Focus the given node. + /// </summary> + public async Task focusAsync() + { + await tab.devtools.SendCommandAsync(new focus + { + nodeId = NodeId + }); + } + + /// <summary> + /// Returns attributes for the specified node. + /// </summary> + /// <returns></returns> + public async Task<Dictionary<string, string>> getAttributesAsync() + { + var commandResponse = await tab.devtools.SendCommandAsync<getAttributes, getAttributesCommandResponse>(new getAttributes + { + nodeId = NodeId + }); + Node.attributes = commandResponse.attributes; + return commandResponse.attributes; + } + + /// <summary> + /// Returns boxes for the currently selected nodes. + /// </summary> + /// <returns></returns> + public async Task<BoxModel> getBoxModelAsync() + { + var commandResponse = await tab.devtools.SendCommandAsync<getBoxModel, getBoxModelCommandResponse>(new getBoxModel + { + nodeId = NodeId + }); + return commandResponse.model; + } + + /// <summary> + /// Returns node's HTML markup. + /// </summary> + /// <returns></returns> + public async Task<string> getOuterHtmlAsync() + { + var commandResponse = await tab.devtools.SendCommandAsync<getOuterHTML, getOuterHTMLCommandResponse>(new getOuterHTML + { + nodeId = NodeId + }); + return commandResponse.outerHTML; + } + + /// <summary> + /// Moves node into an other container node, places it before the given node (if absent, the moved node becomes the last child of `targetNode`). + /// </summary> + /// <param name="targetNode"></param> + /// <param name="insertBeforeNode"></param> + /// <returns></returns> + /// <remarks>TODO: registered event handlers might need to be registered to the new node id again</remarks> + public async Task moveToAsync(DomNodeWrapper targetNode, DomNodeWrapper? insertBeforeNode = null) + { + var commandResponse = await tab.devtools.SendCommandAsync<moveTo, moveToCommandResponse>(new moveTo + { + nodeId = NodeId, + targetNodeId = targetNode.NodeId, + insertBeforeNodeId = insertBeforeNode?.NodeId + }); + // update the new node id of the moved node + // TODO: registered event handlers might need to be registered again to the new node id + tab.domNodes.Remove(NodeId); + NodeId = commandResponse.nodeId; + tab.domNodes[NodeId] = this; + } + + /// <summary> + /// Search a child node by querySelector. + /// </summary> + /// <param name="selector"></param> + /// <returns></returns> + public async Task<DomNodeWrapper> querySelectorAsync(string selector) + { + var commandResponse = await tab.devtools.SendCommandAsync<querySelector, querySelectorCommandResponse>(new querySelector + { + nodeId = NodeId, + selector = selector + }); + return createOrUpdateNode(tab, commandResponse.nodeId); + } + /// <summary> + /// Search all child nodes matching the querySelector. + /// </summary> + /// <param name="selector"></param> + /// <returns></returns> public async Task<DomNodeWrapper[]> querySelectorAllAsync(string selector) { var commandResponse = await tab.devtools.SendCommandAsync<querySelectorAll, querySelectorAllCommandResponse>(new querySelectorAll @@ -235,15 +380,134 @@ namespace bessw.Unity.WebView.ChromeDevTools return domNodes; } + /// <summary> + /// Removes attribute with given name from the element. + /// </summary> + /// <param name="name"></param> + /// <returns></returns> + public async Task removeAttributeAsync(string name) + { + await tab.devtools.SendCommandAsync(new removeAttribute + { + nodeId = NodeId, + name = name + }); + } - public async Task<Dictionary<string, string>> getAttributesAsync() + /// <summary> + /// Requests that children of the node with given id are returned to the caller in form of + /// `setChildNodes` events where not only immediate children are retrieved, but all children down + /// to the specified depth. + /// </summary> + public async Task requestChildNodesAsync(int? depth = null, bool? pierce = null) { - var commandResponse = await tab.devtools.SendCommandAsync<getAttributes, getAttributesCommandResponse>(new getAttributes + await tab.devtools.SendCommandAsync(new requestChildNodes + { + nodeId = NodeId, + depth = depth, + pierce = pierce + }); + } + + /// <summary> + /// Scrolls the node into view if not already visible. + /// </summary> + /// <returns></returns> + public async Task scrollIntoViewIfNeededAsync() + { + await tab.devtools.SendCommandAsync(new scrollIntoViewIfNeeded { nodeId = NodeId }); - Node.attributes = commandResponse.attributes; - return commandResponse.attributes; + } + + /// <summary> + /// Sets attribute for the node. This method is useful when user edits some existing + /// attribute value and types in several attribute name/value pairs. + /// </summary> + public async Task setAttributesAsText(string text, string? name = null) + { + await tab.devtools.SendCommandAsync(new setAttributesAsText + { + nodeId = NodeId, + text = text, + name = name + }); + } + + /// <summary> + /// Sets attribute for this element. + /// </summary> + /// <param name="name"></param> + /// <param name="value"></param> + /// <returns></returns> + public async Task setAttributeValue(string name, string value) + { + await tab.devtools.SendCommandAsync(new setAttributeValue + { + nodeId = NodeId, + name = name, + value = value + }); + } + + /// <summary> + /// Sets files for the given file input element. + /// </summary> + public Task setFileInputFilesAsync(string[] files) + { + return tab.devtools.SendCommandAsync(new setFileInputFiles + { + nodeId = NodeId, + files = files + }); + } + + /// <summary> + /// Sets the nodes name. + /// </summary> + /// <param name="name"></param> + /// <returns></returns> + /// <remarks>updates the nodeId</remarks> + public async Task setNodeNameAsync(string name) + { + var commandResponse = await tab.devtools.SendCommandAsync<setNodeName,setNodeNameCommandResponse>(new setNodeName + { + nodeId = NodeId, + name = name + }); + // update the new node id of the renamed node + // TODO: registered event handlers might need to be registered again to the new node id + tab.domNodes.Remove(NodeId); + NodeId = commandResponse.nodeId; + tab.domNodes[NodeId] = this; + } + + public Task setNodeValueAsync(string value) + { + return tab.devtools.SendCommandAsync(new setNodeValue + { + nodeId = NodeId, + value = value + }); + } + + /// <summary> + /// Sets node HTML markup, changes the node id. + /// </summary> + public async Task setOuterHtmlAsync(string outerHtml) + { + var commandResponse = await tab.devtools.SendCommandAsync<setOuterHTML,setOuterHTMLCommandResponse>(new setOuterHTML + { + nodeId = NodeId, + outerHTML = outerHtml + }); + + // update the new node id of the renamed node + // TODO: registered event handlers might need to be registered again to the new node id + tab.domNodes.Remove(NodeId); + NodeId = commandResponse.nodeId; + tab.domNodes[NodeId] = this; } #endregion Async Commands diff --git a/Runtime/WebViewComponent.cs b/Runtime/WebViewComponent.cs index 7865670bf11799fdb46fbb44b9d4edcf026350d3..c6ff868276bd3b41f8d59a12ee1722821fc6b0ff 100644 --- a/Runtime/WebViewComponent.cs +++ b/Runtime/WebViewComponent.cs @@ -1,12 +1,15 @@ using bessw.Unity.WebView.ChromeDevTools; +using bessw.Unity.WebView.ChromeDevTools.Protocol.DOM; using bessw.Unity.WebView.ChromeDevTools.Protocol.Input; using MoreLinq; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; using UnityEngine; +using UnityEngine.Events; using UnityEngine.EventSystems; using UnityEngine.UI; @@ -43,6 +46,16 @@ namespace bessw.Unity.WebView private Browser browser; public BrowserTab tab; + /// <summary> + /// Fired when `Document` has been totally updated. Node ids are no longer valid. + /// Other DOM events are fired directly on their `DomNodeWrapper` object. + /// </summary> + public event Action<documentUpdatedEvent> onDomDocumentUpdated + { + add => tab.devtools.onDomDocumentUpdated += value; + remove => tab.devtools.onDomDocumentUpdated -= value; + } + private RawImage rawImage; private RectTransform rectTransform;