XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using Jint;
using Jint.Native;
using Jint.Native.Object;

namespace XFESystem.Browser;

public sealed partial class DomBridge
{
    private sealed class FormSelection { public int Start; public int End; public string Direction = "none"; }
    private readonly DependentIdentityMap<IElement,FormSelection> selections = new();
    public IElement? CurrentScript { get; set; }

    public void FocusNode(INode? node)
    {
        var element = node as IElement ?? node?.ParentElement;
        if (element is null)
        {
            if (focused is not null) engine.Invoke(Wrap(focused).AsObject().Get("blur"), Wrap(focused), []);
            engine.Evaluate("void 0");
            return;
        }
        // A page's mousedown listener may have focused its hidden input. A nonfocusable
        // paint/hit-test node must not overwrite that focus after event dispatch.
        while (element is not null && !(element.LocalName is "input" or "textarea" or "button" or "select" || element.HasAttribute("tabindex") || element.HasAttribute("contenteditable") || element.LocalName == "a" && element.HasAttribute("href"))) element = element.ParentElement;
        if (element is null || element.HasAttribute("disabled")) return;
        engine.Invoke(Wrap(element).AsObject().Get("focus"), Wrap(element), []);
        engine.Evaluate("void 0");
    }

    /// <summary>Default editable-control action after the caller has dispatched cancelable beforeinput.</summary>
    public bool ApplyTextInput(string text, int deleteDirection = 0)
    {
        ArgumentNullException.ThrowIfNull(text);
        if (focused is not IElement element || element is not (IHtmlInputElement or IHtmlTextAreaElement) || element.HasAttribute("disabled") || element.HasAttribute("readonly")) return false;
        if (element is IHtmlInputElement && (text.Contains('\n') || text.Contains('\r'))) return false;
        var value = FormValue(element); var selection = Selection(element);
        var start = Math.Clamp(selection.Start, 0, value.Length); var end = Math.Clamp(selection.End, start, value.Length);
        if (start == end)
        {
            if (deleteDirection < 0 && start > 0) { --start; if (start > 0 && char.IsLowSurrogate(value[start]) && char.IsHighSurrogate(value[start - 1])) --start; }
            if (deleteDirection > 0 && end < value.Length) { ++end; if (end < value.Length && char.IsHighSurrogate(value[end - 1]) && char.IsLowSurrogate(value[end])) ++end; }
        }
        var next = value[..start] + text + value[end..];
        if (next == value) return false;
        SetFormValue(element, next); selection.Start = selection.End = start + text.Length;
        DispatchEvent(element, "input", new() { Cancelable = false, Data = text, InputType = deleteDirection < 0 ? "deleteContentBackward" : deleteDirection > 0 ? "deleteContentForward" : text == "\n" ? "insertLineBreak" : "insertText" });
        SelectionChanged(element, true);
        return true;
    }

    /// <summary>Default control action after an uncanceled keydown. Page handlers (including Monaco) get first refusal.</summary>
    public bool ApplyControlKey(string key, bool shift = false, bool control = false)
    {
        if (key == "Tab" && !control)
        {
            var focusable = document.QuerySelectorAll("input:not([disabled]),textarea:not([disabled]),button:not([disabled]),select:not([disabled]),a[href],[tabindex]")
                .Where(element => element.GetAttribute("tabindex") != "-1" && !element.HasAttribute("hidden")).ToArray();
            if (focusable.Length == 0) return false; var index = Array.IndexOf(focusable, focused);
            FocusNode(focusable[(index + (shift ? -1 : 1) + focusable.Length) % focusable.Length]); return true;
        }
        if (focused is not IElement element || element is not (IHtmlInputElement or IHtmlTextAreaElement) || element.HasAttribute("disabled")) return false;
        var value = FormValue(element); var selection = Selection(element);
        int start = Math.Clamp(selection.Start, 0, value.Length), end = Math.Clamp(selection.End, start, value.Length);
        if (control && key.Equals("a", StringComparison.OrdinalIgnoreCase))
        { SetSelection(element, 0, value.Length, "forward"); SelectionChanged(element, true); return true; }
        if (key == "Enter")
        {
            if (element is not IHtmlTextAreaElement || element.HasAttribute("readonly")) return false;
            return DispatchEvent(element, "beforeinput", new() { Data = "\n", InputType = "insertLineBreak" }) && ApplyTextInput("\n");
        }
        var anchor = selection.Direction == "backward" ? end : start;
        var caret = selection.Direction == "backward" ? start : end;
        int target;
        if (key is "ArrowLeft" or "ArrowRight")
        {
            var left = key == "ArrowLeft";
            target = !shift && start != end ? left ? start : end : control ? WordBoundary(value, caret, left) : CodePointBoundary(value, caret, left);
        }
        else if (key is "Home" or "End")
        {
            target = key == "Home" ? control || element is IHtmlInputElement ? 0 : LineStart(value, caret) : control || element is IHtmlInputElement ? value.Length : LineEnd(value, caret);
        }
        else if (key is "ArrowUp" or "ArrowDown" or "PageUp" or "PageDown")
        {
            if (element is not IHtmlTextAreaElement) return false;
            int count = key is "PageUp" or "PageDown" ? Math.Max(1, (int)((host.Measure?.Invoke(element).Height ?? 160) / 20)) : 1;
            int column = caret - LineStart(value, caret); target = caret;
            for (var step = 0; step < count; ++step)
            {
                if (key is "ArrowUp" or "PageUp") { var line = LineStart(value, target); if (line == 0) { target = 0; break; } var previous = LineStart(value, line - 1); target = Math.Min(previous + column, line - 1); }
                else { var line = LineEnd(value, target); if (line == value.Length) { target = value.Length; break; } var next = line + 1; target = Math.Min(next + column, LineEnd(value, next)); }
            }
        }
        else return false;
        if (shift) SetSelection(element, Math.Min(anchor, target), Math.Max(anchor, target), target < anchor ? "backward" : "forward");
        else SetSelection(element, target, target, "none");
        SelectionChanged(element, true); return true;
    }

    private void SelectionChanged(IElement element, bool external)
    {
        host.Mutated?.Invoke();
        if (external) { DispatchEvent(element, "select", new() { Cancelable = false }); DispatchEvent(document, "selectionchange", new() { Bubbles = false, Cancelable = false }); }
        else { DispatchInternal(element, "select", true, false); DispatchInternal(document, "selectionchange", false, false); }
    }
    private static int CodePointBoundary(string value, int position, bool left)
    {
        if (left) { if (position == 0) return 0; --position; if (position > 0 && char.IsLowSurrogate(value[position]) && char.IsHighSurrogate(value[position - 1])) --position; }
        else { if (position == value.Length) return position; ++position; if (position < value.Length && char.IsHighSurrogate(value[position - 1]) && char.IsLowSurrogate(value[position])) ++position; }
        return position;
    }
    private static int WordBoundary(string value, int position, bool left)
    {
        if (left) { while (position > 0 && char.IsWhiteSpace(value[position - 1])) --position; while (position > 0 && !char.IsWhiteSpace(value[position - 1])) --position; }
        else { while (position < value.Length && !char.IsWhiteSpace(value[position])) ++position; while (position < value.Length && char.IsWhiteSpace(value[position])) ++position; }
        return position;
    }
    private static int LineStart(string value, int position) => position <= 0 ? 0 : value.LastIndexOf('\n', Math.Min(position - 1, value.Length - 1)) + 1;
    private static int LineEnd(string value, int position) { var end = value.IndexOf('\n', position); return end < 0 ? value.Length : end; }

    private void InstallForm(ObjectInstance wrapper, IElement element)
    {
        Property(wrapper, "value", () => FormValue(element), value => SetFormValue(element, LimitedText(value)));
        Property(wrapper, "defaultValue", () => element is IHtmlTextAreaElement ? element.TextContent : element.GetAttribute("value") ?? string.Empty,
            value => { if (element is IHtmlTextAreaElement) MutateText(element, value); else SetAttribute(element, "value", value); });
        Property(wrapper, "selectionStart", () => Selection(element).Start, value => SetSelection(element, NumberIndex(value), Selection(element).End, Selection(element).Direction));
        Property(wrapper, "selectionEnd", () => Selection(element).End, value => SetSelection(element, Selection(element).Start, NumberIndex(value), Selection(element).Direction));
        Property(wrapper, "selectionDirection", () => Selection(element).Direction, value => Selection(element).Direction = LimitedText(value));
        Function(wrapper, "setSelectionRange", args => { SetSelection(element, NumberIndex(Value(args, 0)), NumberIndex(Value(args, 1)), args.Length > 2 ? Argument(args, 2) : "none"); return JsValue.Undefined; });
        Function(wrapper, "select", _ => { SetSelection(element, 0, FormValue(element).Length, "none"); return JsValue.Undefined; });
        Function(wrapper, "setRangeText", args =>
        {
            var old = FormValue(element); var selection = Selection(element); var replacement = Argument(args, 0);
            var oldStart = selection.Start; var oldEnd = selection.End; var oldDirection = selection.Direction;
            var start = args.Length >= 3 ? NumberIndex(Value(args, 1)) : selection.Start; var end = args.Length >= 3 ? NumberIndex(Value(args, 2)) : selection.End;
            start = Math.Clamp(start, 0, old.Length); end = Math.Clamp(end, start, old.Length);
            SetFormValue(element, old[..start] + replacement + old[end..]);
            var mode = args.Length > 3 ? Argument(args, 3) : "preserve";
            if (mode == "select") SetSelection(element, start, start + replacement.Length, "none");
            else if (mode == "start") SetSelection(element, start, start, "none");
            else if (mode == "end") SetSelection(element, start + replacement.Length, start + replacement.Length, "none");
            else { int delta = replacement.Length - (end - start); SetSelection(element, oldStart > end ? oldStart + delta : Math.Min(oldStart, start), oldEnd > end ? oldEnd + delta : Math.Min(oldEnd, start + replacement.Length), oldDirection); }
            return JsValue.Undefined;
        });
    }

    private static string FormValue(IElement element) => element switch { IHtmlInputElement input => input.Value, IHtmlTextAreaElement area => area.Value, _ => element.GetAttribute("value") ?? string.Empty };
    private void SetFormValue(IElement element, string value)
    {
        if (value.Length > limits.MaximumHtmlCharacters) throw new BrowserLimitException("The control value exceeds its character limit.");
        var old = FormValue(element);
        if (element is IHtmlInputElement input) input.Value = value;
        else if (element is IHtmlTextAreaElement area) area.Value = value;
        else { SetAttribute(element, "value", value); return; }
        try { ValidateBudget(); }
        catch { if (element is IHtmlInputElement rollbackInput) rollbackInput.Value = old; else if (element is IHtmlTextAreaElement rollbackArea) rollbackArea.Value = old; throw; }
        var selection = Selection(element); selection.Start = selection.End = value.Length;
        host.Mutated?.Invoke();
    }
    private FormSelection Selection(IElement element)
    {
        if (!selections.TryGetValue(element, out var state)) selections.Add(element, state = new() { Start = FormValue(element).Length, End = FormValue(element).Length });
        return state;
    }
    private void SetSelection(IElement element, int start, int end, string direction)
    { var state = Selection(element); var length = FormValue(element).Length; state.End = Math.Clamp(end, 0, length); state.Start = Math.Clamp(start, 0, state.End); state.Direction = direction is "forward" or "backward" ? direction : "none"; }
    private static int NumberIndex(JsValue value) => value.IsNumber() && double.IsFinite(value.AsNumber()) ? (int)Math.Clamp(value.AsNumber(), 0, int.MaxValue) : 0;
}