Select Git revision
AttributeModifiedEvent.cs
WebViewController.cs 11.61 KiB
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;
}
}