From 86a3da614929d59db1680d5640a669a3011cad0a Mon Sep 17 00:00:00 2001 From: baletiballo <75846481+baletiballo@users.noreply.github.com> Date: Thu, 23 Jan 2025 17:42:21 +0100 Subject: [PATCH] Have JS extract all data from the rendered scroll This made a lot of C# Code obsolete. Moved it into individual obsolete functions and created a region for them. --- Assets/Scripts/UI/InGame/WebViewController.cs | 211 +++++++++--------- .../scroll_interaction/.SetScrollContent.js | 33 --- .../scroll_interaction/SetScrollContent.js | 45 ++-- 3 files changed, 136 insertions(+), 153 deletions(-) delete mode 100644 Assets/StreamingAssets/ScrollView_Server/scroll_interaction/.SetScrollContent.js diff --git a/Assets/Scripts/UI/InGame/WebViewController.cs b/Assets/Scripts/UI/InGame/WebViewController.cs index 103d3606..5b5a7b12 100644 --- a/Assets/Scripts/UI/InGame/WebViewController.cs +++ b/Assets/Scripts/UI/InGame/WebViewController.cs @@ -12,7 +12,6 @@ using System.Linq; using System.Text.RegularExpressions; using UnityEngine; -using UnityEngine.UIElements; public class WebViewController : ScrollView { @@ -64,7 +63,7 @@ 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; @@ -79,94 +78,48 @@ private void DeRegisterBrowserEventhandlers() /// <summary> /// <para> - /// Hand the ScrollView the labels (and potentially assignments) of all facts, so it can render the scroll content. (And then tell it to do so) - /// </para><para> - /// More precisely: Put a stringified and Json serialized Dictionary - /// <see cref="OMS.uri"/> slotID -> (<see cref="string"/> label, <see cref="Fact.Id"/> factId) <br/> - /// into the "data-assignments" attribute of the "assignments" node + /// Hand the ScrollView the (partially applied) scroll, and request it to rerender the description. /// </para> /// </summary> - /// <param name="scroll"></param> - private async void SetScrollContent(Scroll scroll) + /// <param name="scroll">The (partially applied) scroll to be rendered</param> + private 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.*")) + // If the description is plaintext, create a generic MathML version + if (!Regex.IsMatch(scroll.description, ".*<scroll-description.*")) { - // 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>"; - + scroll.description = CreateGenericScrollDescription(scroll); } - //else // Is done by JS. How to render the scroll is the scrolls buisness - //{ - // // 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>"; - // }); - //} - - // Display the scroll description. - await scrollContainer.setOuterHtmlAsync($"<div id='scrollContainer'>{description}</div>"); - - DomNodeWrapper assignmentsNode = await document.querySelectorAsync("#assignments"); - Dictionary<string, (string, string)> assignments = new(); - - foreach (MMTFact fact in scroll.requiredFacts) - { - string slotId = fact.@ref.uri; - ActiveScroll.SlotAssignment assignment = SwitchScrollUI.activeScrollData.Assignments[slotId]; - if (assignment.IsSet) - { - assignments.Add(slotId, (assignment.fact.GetLabel(StageStatic.stage.factState), assignment.fact.Id)); - } - else - { - assignments.Add(slotId, (fact.label, "")); - } - } - foreach (MMTFact fact in scroll.acquiredFacts) - { - assignments.Add(fact.@ref.uri, (fact.label, "")); - } - await assignmentsNode.setAttributeValue("data-assignments", JsonConvert.SerializeObject(assignments)); + ForwardDataToScrollView("scrollDynamic", scroll); _ = webViewComponent.tab.Evaluate("RenderScroll()"); + Debug.Log("Called for scrollView to rerender"); // Update the interactive components dropzones = null; RegisterBrowserEventhandlers(); } + /// <summary> + /// Create a MathML description for a scroll without one. + /// Obviously cannot be very fancy. + /// </summary> + /// <param name="scroll">The rendered scroll to create a description for</param> + /// <returns>A fitting scroll-description element as a string</returns> + private string CreateGenericScrollDescription(Scroll scroll) + { + // 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>"); + + return $"<scroll-description><p>{scroll.description}</p><div id='legacySlots'>{String.Join("", factSlots)}</div></scroll-description>"; + } + private async void DropzoneAttributeModifiedHandler(attributeModifiedEvent attributeModifiedEvent) { //Debug.Log($"dropzoneAttributeModifiedHandler: '{attributeModifiedEvent.name}'"); @@ -176,7 +129,7 @@ private async void DropzoneAttributeModifiedHandler(attributeModifiedEvent attri { // get the slot id var node = await webViewComponent.tab.GetNode(attributeModifiedEvent.nodeId); - if (! (node.Node.attributes.TryGetValue("data-slot-id", out string slot) + 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"); @@ -229,6 +182,33 @@ private void GetHintHandler(string url) SwitchScrollUI.activeScrollData.ButtonClicked(new HintScrollButton(url)); } + /// <summary> + /// Send data to the ScrollView. + /// <para>JSON serialize the <paramref name="dataValue"/> and add it to the dataset of the Unity-Data-Interface Node. JS can then retrieve it by calling + /// <code>const unityData = JSON.parse(document.querySelector("#Unity-Data-Interface").dataset.<paramref name="dataID"/>);</code> + /// </para> + /// </summary> + /// <param name="dataID">The name of the dataset-member <paramref name="dataValue"/> will be assigned to</param> + /// <param name="dataValue">The data to transmit</param> + public async void ForwardDataToScrollView(string dataID, object dataValue) + { + string payload = JsonConvert.SerializeObject(dataValue); + //payload = WebUtility.HtmlEncode(payload); + // Using the DomNodeWrapper methods requires 3 requests, and since those go to an altogether + // different prozess, I would like to avoid that. + await webViewComponent.tab.Evaluate( + $"document.querySelector('#Unity-Data-Interface').dataset.{dataID} = '{payload}'" + ); + } + + public string[] GetFactAssignments() + { + return dropzones.Select(dropzone => dropzone.Node.attributes.GetValueOrDefault("data-fact-id", null)).ToArray(); + } + + #region ObsoleteFunctions : Functions that are no longer used. Since similar functionality may be of use sometime later, they are here for reference. + + /// <summary> /// <para> /// Hand the ScrollView the labels (and potentially assignments) of all facts, so it can render the scroll. @@ -239,36 +219,62 @@ private void GetHintHandler(string url) /// </para> /// </summary> /// <param name="youHappyNowJS">Is ignored. JS bindings need to take a string argument, so here it is.</param> - [Obsolete("This data can be aquired directly from the MMTServer")] - private async void UpdateAssignmentsHandler(string youHappyNowJS = "") + [Obsolete("This data needs to be updated iff the assignment is changed in Unity, JS doesn't need a way to to request it independently.")] + private void UpdateAssignmentsHandler(string youHappyNowJS = "") { - DomNodeWrapper document = await webViewComponent.tab.Document; - DomNodeWrapper assignmentsNode = await document.querySelectorAsync("#assignments"); Scroll scroll = SwitchScrollUI.activeScrollData.RenderedScroll; - Dictionary<string, (string,string)> assignments = new(); - foreach (var (slotId,assignment) in SwitchScrollUI.activeScrollData.Assignments) + Dictionary<string, (string, string)> assignments = new(); + + foreach (MMTFact slot in scroll.requiredFacts) { - string 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 */ - string fact_id = assignment.IsSet ? assignment.fact.Id : ""; - assignments.Add(slotId,(label,fact_id)); + string slotId = slot.@ref.uri; + ActiveScroll.SlotAssignment assignment = SwitchScrollUI.activeScrollData.Assignments[slotId]; + if (assignment.IsSet) + { + Fact fact = assignment.fact; + assignments.Add(slotId, (fact.GetLabel(StageStatic.stage.factState), fact.Id)); + } + else + { + assignments.Add(slotId, (slot.label, "")); + } } - foreach(MMTFact fact in scroll.acquiredFacts) + foreach (MMTFact fact in scroll.acquiredFacts) { - string label = scroll.acquiredFacts.Find(f => fact.@ref.uri == f.@ref.uri).label; - Debug.Log(label); - Debug.Log(fact.label); assignments.Add(fact.@ref.uri, (fact.label, "")); } - - - await assignmentsNode.setAttributeValue("data-assignments", JsonConvert.SerializeObject(assignments)); + ForwardDataToScrollView("assignments", JsonConvert.SerializeObject(assignments)); } - public string[] GetFactAssignments() + [Obsolete("Is done by JS. How to render the scroll is the scrolls buisness.")] + private string createCustomScrollDescription(Scroll scroll) { - return dropzones.Select(dropzone => dropzone.Node.attributes.GetValueOrDefault("data-fact-id", null)).ToArray(); + string description = scroll.description; + // replace slot templates + //the scroll-slot syntax has changed by now, so the match would have to be adapted. + 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>"; + }); + + return description; } [Obsolete()] @@ -303,11 +309,13 @@ private IEnumerator GetDropzoneState() Debug.LogWarning($"dropzone pre"); DomNodeWrapper doc = null; - yield return DomNodeWrapper.getDocument(webViewComponent.tab, (document) => { + yield return DomNodeWrapper.getDocument(webViewComponent.tab, (document) => + { Debug.LogWarning($"dropzone 1: '{document}'"); doc = document; - StartCoroutine(document.querySelectorAll("[dropzone='copy']", (dropzones) => { + StartCoroutine(document.querySelectorAll("[dropzone='copy']", (dropzones) => + { foreach (var dropzone in dropzones) { Debug.LogWarning($"dropzone 2: Node is Null?: '{dropzone.Node == null}'"); @@ -320,8 +328,11 @@ private IEnumerator GetDropzoneState() }); Debug.LogWarning($"dropzone post: '{doc}'"); } + #endregion ObsoleteFunctions } + + public class FactObjectUIConverter : JsonConverter<FactObjectUI> { public override void WriteJson(JsonWriter writer, FactObjectUI value, JsonSerializer serializer) diff --git a/Assets/StreamingAssets/ScrollView_Server/scroll_interaction/.SetScrollContent.js b/Assets/StreamingAssets/ScrollView_Server/scroll_interaction/.SetScrollContent.js deleted file mode 100644 index 7fcf8b06..00000000 --- a/Assets/StreamingAssets/ScrollView_Server/scroll_interaction/.SetScrollContent.js +++ /dev/null @@ -1,33 +0,0 @@ -let currentScrollRef = "" // ref of the scroll currently displayed - -function RenderScroll() { - const scroll = JSON.parse(document.querySelector("#Unity-Data-Interface").dataset.scrollDynamic); - console.log(scroll.requiredFacts); - const scrollContainer = document.querySelector("#scrollContainer"); - - // replace the description if the scroll changed, otherwise only its content needs update - if (scroll.ref != currentScrollRef) { - currentScrollRef = scroll.ref; - scrollContainer.innerHTML = scroll.description; - } - - // go through the facts in the scroll, show their labels and add dropzones - scroll.requiredFacts.forEach(fact => { - $("[data-slot-id='" + fact.ref.uri + "']") - .text(fact.label) - .attr("dropzone", "copy") - //.attr("data-fact-id", fact.df == null ? "" : fact.df.ref.uri) - }) - - // acquired facts only need updated labels - scroll.acquiredFacts.forEach(fact => { - $("[data-solution-id='" + fact.ref.uri + "']") - .text(fact.label) - }) - console.log(scroll.label + 'Scroll rendered') -} - - -$(function () { - RenderScroll() -}); \ No newline at end of file diff --git a/Assets/StreamingAssets/ScrollView_Server/scroll_interaction/SetScrollContent.js b/Assets/StreamingAssets/ScrollView_Server/scroll_interaction/SetScrollContent.js index b91bd379..7fcf8b06 100644 --- a/Assets/StreamingAssets/ScrollView_Server/scroll_interaction/SetScrollContent.js +++ b/Assets/StreamingAssets/ScrollView_Server/scroll_interaction/SetScrollContent.js @@ -1,28 +1,33 @@ +let currentScrollRef = "" // ref of the scroll currently displayed function RenderScroll() { + const scroll = JSON.parse(document.querySelector("#Unity-Data-Interface").dataset.scrollDynamic); + console.log(scroll.requiredFacts); const scrollContainer = document.querySelector("#scrollContainer"); - const description = scrollContainer.textContent; - const assignments = JSON.parse(document.querySelector("#assignments").dataset.assignments) - console.log(assignments); - //everything that has a slotID is a scroll slot - scrollContainer.querySelectorAll("[data-slot-id]").forEach(element => { - //console.log(element); - let slotId = element.dataset.slotId; - let fact_assignment = assignments[slotId]; + // replace the description if the scroll changed, otherwise only its content needs update + if (scroll.ref != currentScrollRef) { + currentScrollRef = scroll.ref; + scrollContainer.innerHTML = scroll.description; + } - element.setAttribute("dropzone", "copy"); - element.textContent = fact_assignment.Item1; - - if (fact_assignment.Item2 != "") { - element.dataset.factId = fact_assignment.Item2; - } - }); + // go through the facts in the scroll, show their labels and add dropzones + scroll.requiredFacts.forEach(fact => { + $("[data-slot-id='" + fact.ref.uri + "']") + .text(fact.label) + .attr("dropzone", "copy") + //.attr("data-fact-id", fact.df == null ? "" : fact.df.ref.uri) + }) - //everything that has a solutionID is a scroll solution - scrollContainer.querySelectorAll("[data-solution-id]").forEach(element => { - element.textContent = assignments[element.dataset.solutionId].Item1; + // acquired facts only need updated labels + scroll.acquiredFacts.forEach(fact => { + $("[data-solution-id='" + fact.ref.uri + "']") + .text(fact.label) }) - console.log('Scroll rendered') + console.log(scroll.label + 'Scroll rendered') } -RenderScroll() \ No newline at end of file + + +$(function () { + RenderScroll() +}); \ No newline at end of file -- GitLab