Skip to content
Snippets Groups Projects
WebViewController.cs 11.4 KiB
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 REST_JSON_API;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
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;
                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's avatar
Björn Eßwein committed
            // 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 =>
Björn Eßwein's avatar
Björn Eßwein committed
                    @$"<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>");

Björn Eßwein's avatar
Björn Eßwein committed
            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)))
            {
Björn Eßwein's avatar
Björn Eßwein committed
                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's avatar
Björn Eßwein committed
            // 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))
            {
Björn Eßwein's avatar
Björn Eßwein committed
                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)
    {
Björn Eßwein's avatar
Björn Eßwein committed
        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();
    }

    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;
    }
}