using bessw.Unity.WebView; using bessw.Unity.WebView.ChromeDevTools; using bessw.Unity.WebView.ChromeDevTools.Protocol.DOM; using MoreLinq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using REST_JSON_API; using System; using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using UnityEngine; public class WebViewController : ScrollView { private WebViewComponent webViewComponent; private DomNodeWrapper[] dropzones; private void Awake() { webViewComponent = GetComponent<WebViewComponent>(); WebViewComponent.serializerSettings.Converters.Add(new FactObjectUIConverter()); webViewComponent.targetUrl = Path.Join(CommunicationEvents.Get_DataPath(), "scrollView.html"); webViewComponent.OnWebViewComponentReady += OnWebViewComponentReady; // TODO: handle webViewComponent.onDomDocumentUpdated } private void OnWebViewComponentReady() { webViewComponent.onDomDocumentUpdated += DocumentUpdatedHandler; SwitchScrollUI.activeScrollData.OnScrollChanged.AddListener(SetScrollContent); SwitchScrollUI.activeScrollData.OnScrollDynamicInfoUpdated.AddListener(SetScrollContent); SwitchScrollUI.activeScrollData.HintAvailableEvent.AddListener(OnHintAvailable); webViewComponent.tab.AddJSBinding("applyScroll", ApplyScrollHandler); webViewComponent.tab.AddJSBinding("getHint", GetHintHandler); if (SwitchScrollUI.activeScrollData.Scroll is not null) { SetScrollContent(SwitchScrollUI.activeScrollData.Scroll); } RegisterBrowserEventhandlers(); } private void OnDisable() { webViewComponent.onDomDocumentUpdated -= DocumentUpdatedHandler; SwitchScrollUI.activeScrollData.OnScrollChanged.RemoveListener(SetScrollContent); SwitchScrollUI.activeScrollData.OnScrollDynamicInfoUpdated.RemoveListener(SetScrollContent); webViewComponent.tab.RemoveJSBinding("applyScroll"); webViewComponent.tab.RemoveJSBinding("getHint"); DeRegisterBrowserEventhandlers(); } private void DocumentUpdatedHandler(documentUpdatedEvent _) { // all old DomNodeWrapper objects are invalid, because the whole document has been updated dropzones = null; RegisterBrowserEventhandlers(); } private async void RegisterBrowserEventhandlers() { // register js event handlers in the browser _ = webViewComponent.tab.Evaluate("addDropZoneEventListeners()"); // register c# event handlers // get the dropzones from the scroll and add the event handler to track when something is dropped DomNodeWrapper document = await webViewComponent.tab.Document; dropzones = await document.querySelectorAllAsync("[dropzone='copy']"); dropzones.ForEach(dropzone => dropzone.OnAttributeModified += DropzoneAttributeModifiedHandler); } private void DeRegisterBrowserEventhandlers() { dropzones?.ForEach(dropzone => dropzone.OnAttributeModified -= DropzoneAttributeModifiedHandler); } /// <summary> /// sets or updates the content of the scroll container dom element /// </summary> /// <param name="scroll"></param> private async void SetScrollContent(Scroll scroll) { // update scroll container content DomNodeWrapper document = await webViewComponent.tab.Document; DomNodeWrapper scrollContainer = await document.querySelectorAsync("#scrollContainer"); var description = scroll.description; if (Regex.IsMatch(description, ".*<scroll-description.*")) { // replace slot templates description = Regex.Replace(description, "<scroll-slot([^>]*)>([^<]*)</scroll-slot>", match => { var extraAttributes = match.Groups[1].Value; var slotId = match.Groups[2].Value; var assignment = SwitchScrollUI.activeScrollData.Assignments[slotId]; /** label of the assigned fact, or the slot label if nothing assigned */ var label = assignment.IsSet ? assignment.fact.GetLabel(StageStatic.stage.factState) : scroll.requiredFacts.Find(fact => fact.@ref.uri == slotId).label; /** id of the assigned fact. If nothing is assigned don't add the attribute */ var fact_id = assignment.IsSet ? $"data-fact-id='{assignment.fact.Id}'" : ""; return $"<mi dropzone='copy' data-slot-id='{slotId}' {fact_id} {extraAttributes}>{label}</mi>"; }); // replace solution templates description = Regex.Replace(description, "<scroll-solution([^>]*)>([^<]*)</scroll-solution>", match => { var extraAttributes = match.Groups[1].Value; var solutionId = match.Groups[2].Value; var label = scroll.acquiredFacts.Find(fact => fact.@ref.uri == solutionId).label; return $"<mi data-solution-id='{solutionId}' {extraAttributes}>{label}</mi>"; }); } else { // scroll is a legacy plain text scroll, generate html with slots for the required facts var factSlots = SwitchScrollUI.activeScrollData.Assignments .Where(pair => pair.Value.IsVisible) .Select(pair => @$"<span class='legacySlot' dropzone='copy' data-slot-id='{pair.Key}' {(pair.Value.IsSet ? $"data-fact-id='{pair.Value.fact.Id}'" : "")}> {(pair.Value.IsSet ? pair.Value.fact.GetLabel(StageStatic.stage.factState) : scroll.requiredFacts.Find(fact => fact.@ref.uri == pair.Key).label )} </span>"); description = $"<scroll-description><p>{scroll.description}</p><div id='legacySlots'>{String.Join("", factSlots)}</div></scroll-description>"; } // display the scroll description dropzones = null; await scrollContainer.setOuterHtmlAsync($"<div id='scrollContainer'>{description}</div>"); RegisterBrowserEventhandlers(); } private async void DropzoneAttributeModifiedHandler(attributeModifiedEvent attributeModifiedEvent) { //Debug.Log($"dropzoneAttributeModifiedHandler: '{attributeModifiedEvent.name}'"); // call the onFactAssignment event if the data-fact-id attribute was modified if (attributeModifiedEvent.name == "data-fact-id") { // get the slot id var node = await webViewComponent.tab.GetNode(attributeModifiedEvent.nodeId); if (! (node.Node.attributes.TryGetValue("data-slot-id", out string slot) || (await node.getAttributesAsync()).TryGetValue("data-slot-id", out slot))) { Debug.LogError($"dropzoneAttributeModifiedHandler: data-slot-id attribute not found on dropzone"); throw new Exception("data-slot-id attribute not found on dropzone"); } // if fact has been unassigned from the scroll if (attributeModifiedEvent.value == "null") { // remove fact from slot SwitchScrollUI.activeScrollData.AssignFact(slot, null); return; } // get the fact from the fact id if (!FactRecorder.AllFacts.TryGetValue(attributeModifiedEvent.value, out Fact fact)) { Debug.LogError($"dropzoneAttributeModifiedHandler: fact with id '{attributeModifiedEvent.value}' not found"); throw new Exception($"fact with id '{attributeModifiedEvent.value}' not found"); } // assign fact to slot SwitchScrollUI.activeScrollData.AssignFact(slot, fact); } } private async void OnHintAvailable(IReadOnlyList<string> url) { dropzones = await (await webViewComponent.tab.Document).querySelectorAllAsync("[dropzone='copy']"); foreach (var dropzone in dropzones) { if (url.Contains(dropzone.Node.attributes["data-slot-id"])) { _ = dropzone.setAttributeValue("data-hint-available", "true"); } else if (dropzone.Node.attributes.ContainsKey("data-hint-available")) { _ = dropzone.removeAttributeAsync("data-hint-available"); } } } private void ApplyScrollHandler(string button) { SwitchScrollUI.activeScrollData.ButtonClicked(new ApplyScrollButton()); } private void GetHintHandler(string url) { SwitchScrollUI.activeScrollData.ButtonClicked(new HintScrollButton(url)); } public string[] GetFactAssignments() { return dropzones.Select(dropzone => dropzone.Node.attributes.GetValueOrDefault("data-fact-id", null)).ToArray(); } [Obsolete()] private async void GetDropzoneStateAsync() { // alternative way to get the dropzone state DomNodeWrapper document = await DomNodeWrapper.getDocumentAsync(webViewComponent.tab); Debug.LogWarning($"dropzone 1: '{document}'"); var dropzones = await document.querySelectorAllAsync("[dropzone='copy']"); Debug.LogWarning($"dropzone 2: '{dropzones.Index().Aggregate("", (currentString, dropzone) => $"{currentString}, {dropzone.Key}: {dropzone.Value.Node.attributes.GetValueOrDefault("data-fact-id")}")}'"); string[] factIDs = new string[dropzones.Length]; // get attributes for each dropzone //for (int i = 0; i < dropzones.Length; i++) //{ // factIDs[i] = ( await dropzones[i].getAttributesAsync() ).GetValueOrDefault("data-fact-id", null); //} //Debug.LogWarning($"dropzone 3: '{string.Join(", ", factIDs)}'"); } /// <summary> /// First try to get the dropzone state using coroutines. /// Doesn't work because coroutines can't access method local temporary variables. /// </summary> /// <returns></returns> [Obsolete("Use getDropzoneStateAsync instead")] private IEnumerator GetDropzoneState() { Debug.LogWarning($"dropzone pre"); DomNodeWrapper doc = null; yield return DomNodeWrapper.getDocument(webViewComponent.tab, (document) => { Debug.LogWarning($"dropzone 1: '{document}'"); doc = document; StartCoroutine(document.querySelectorAll("[dropzone='copy']", (dropzones) => { foreach (var dropzone in dropzones) { Debug.LogWarning($"dropzone 2: Node is Null?: '{dropzone.Node == null}'"); StartCoroutine(dropzone.getAttributes((attributes) => { Debug.LogWarning($"dropzone 3 getAttributes: '{string.Join(", ", attributes.Values)}'"); })); } })); }); Debug.LogWarning($"dropzone post: '{doc}'"); } } public class FactObjectUIConverter : JsonConverter<FactObjectUI> { public override void WriteJson(JsonWriter writer, FactObjectUI value, JsonSerializer serializer) { //serializer.Serialize(writer, value.Fact); JObject o = JObject.FromObject(value.Fact, WebViewComponent.serializer); o.AddFirst(new JProperty("id", value.Fact.Id)); o.WriteTo(writer); } public override FactObjectUI ReadJson(JsonReader reader, Type objectType, FactObjectUI existingValue, bool hasExistingValue, JsonSerializer serializer) { var factObject = new FactObjectUI(); factObject.Fact = serializer.Deserialize<Fact>(reader); return factObject; } }