Newer
Older
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 System;
using System.Collections;
using System.Collections.Generic;
Björn Eßwein
committed
using System.IO;
Björn Eßwein
committed
using System.Text.RegularExpressions;
Björn Eßwein
committed
public class WebViewController : ScrollView
{
private WebViewComponent webViewComponent;
private DomNodeWrapper[] dropzones;
private void Awake()
{
webViewComponent = GetComponent<WebViewComponent>();
WebViewComponent.serializerSettings.Converters.Add(new FactObjectUIConverter());
Björn Eßwein
committed
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);
Björn Eßwein
committed
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
Björn Eßwein
committed
_ = 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
Björn Eßwein
committed
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>
private async void SetScrollContent(Scroll scroll)
{
// update scroll container content
Björn Eßwein
committed
DomNodeWrapper document = await webViewComponent.tab.Document;
DomNodeWrapper scrollContainer = await document.querySelectorAsync("#scrollContainer");
var description = scroll.description;
Björn Eßwein
committed
if (Regex.IsMatch(description, ".*<scroll-description.*"))
Björn Eßwein
committed
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;
return $"<mi dropzone='copy' data-slot-id='{slotId}' {(assignment.IsSet ? $"data-fact-id='{assignment.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;
return $"<mi data-solution-id='{solutionId}' {extraAttributes}>{scroll.acquiredFacts.Find(fact => fact.@ref.uri == solutionId).label}</mi>";
});
Björn Eßwein
committed
}
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
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");
}
Björn Eßwein
committed
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();
}
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
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;
}
}