From 1a46ccbbeb21b220d408c1422c55daed2e6d276b Mon Sep 17 00:00:00 2001 From: Max Katz Date: Thu, 13 Apr 2023 14:51:11 -0400 Subject: [PATCH] Add "readClipboardText" workaround for Mozilla --- src/Browser/Avalonia.Browser/AvaloniaView.cs | 2 + .../Avalonia.Browser/Interop/InputHelper.cs | 5 +- .../webapp/modules/avalonia/input.ts | 59 ++++++++++++++++++- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/Browser/Avalonia.Browser/AvaloniaView.cs b/src/Browser/Avalonia.Browser/AvaloniaView.cs index 76947c949c..8e1f643b3e 100644 --- a/src/Browser/Avalonia.Browser/AvaloniaView.cs +++ b/src/Browser/Avalonia.Browser/AvaloniaView.cs @@ -90,6 +90,8 @@ namespace Avalonia.Browser _topLevel.Renderer.Start(); + InputHelper.InitializeBackgroundHandlers(); + InputHelper.SubscribeKeyEvents( _containerElement, OnKeyDown, diff --git a/src/Browser/Avalonia.Browser/Interop/InputHelper.cs b/src/Browser/Avalonia.Browser/Interop/InputHelper.cs index a978c18f9b..181bfabbe8 100644 --- a/src/Browser/Avalonia.Browser/Interop/InputHelper.cs +++ b/src/Browser/Avalonia.Browser/Interop/InputHelper.cs @@ -81,7 +81,10 @@ internal static partial class InputHelper [JSImport("InputHelper.setBounds", AvaloniaModule.MainModuleName)] public static partial void SetBounds(JSObject htmlElement, int x, int y, int width, int height, int caret); - [JSImport("globalThis.navigator.clipboard.readText")] + [JSImport("InputHelper.initializeBackgroundHandlers", AvaloniaModule.MainModuleName)] + public static partial void InitializeBackgroundHandlers(); + + [JSImport("InputHelper.readClipboardText", AvaloniaModule.MainModuleName)] public static partial Task ReadClipboardTextAsync(); [JSImport("globalThis.navigator.clipboard.writeText")] diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts index fb94352192..bbc59aba1c 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/input.ts @@ -19,14 +19,68 @@ enum RawInputModifiers { PenBarrelButton = 2048 } +/* +* This is a hack to handle Mozilla clipboard events in a more convinient way for framework users. +* In the browser, events go in order KeyDown -> Paste -> KeyUp. +* On KeyDown we trigger Avalonia handlers, which might execute readClipboardText. +* When readClipboardText was executed, we mark ClipboardState as Pending and setup clipboard promise, +* which will un-handle KeyDown event, basically allowing browser to pass a Paste event properly. +* On actual Paste event we execute promise callbacks, resuming async operation, and returning pasted text to the app. +* Note #1, on every KeyUp event we will reset all the state and reject pending promises if any, as this event it expected to come after Paste. +* Note #2, whole this code will be executed only on legacy browsers like Mozilla, where clipboard.readText is not available. +* Note #3, with all of these hacks Clipboard.ReadText will still work only on actual "paste" gesture initiated by user. +* */ +enum ClipboardState { + None, + Ready, + Pending +} + export class InputHelper { + static clipboardState: ClipboardState = ClipboardState.None; + static resolveClipboard?: any; + static rejectClipboard?: any; + + public static initializeBackgroundHandlers() { + if (this.clipboardState !== ClipboardState.None) { + return; + } + + globalThis.addEventListener("paste", (args: any) => { + if (this.clipboardState === ClipboardState.Pending) { + this.resolveClipboard(args.clipboardData.getData("text")); + } + }); + this.clipboardState = ClipboardState.Ready; + } + + public static async readClipboardText(): Promise { + if (globalThis.navigator.clipboard.readText) { + return await globalThis.navigator.clipboard.readText(); + } else { + try { + return await new Promise((resolve, reject) => { + this.clipboardState = ClipboardState.Pending; + this.resolveClipboard = resolve; + this.rejectClipboard = reject; + }); + } finally { + this.clipboardState = ClipboardState.Ready; + this.resolveClipboard = null; + this.rejectClipboard = null; + } + } + } + public static subscribeKeyEvents( element: HTMLInputElement, keyDownCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean, keyUpCallback: (code: string, key: string, modifiers: RawInputModifiers) => boolean) { const keyDownHandler = (args: KeyboardEvent) => { if (keyDownCallback(args.code, args.key, this.getModifiers(args))) { - args.preventDefault(); + if (this.clipboardState !== ClipboardState.Pending) { + args.preventDefault(); + } } }; element.addEventListener("keydown", keyDownHandler); @@ -35,6 +89,9 @@ export class InputHelper { if (keyUpCallback(args.code, args.key, this.getModifiers(args))) { args.preventDefault(); } + if (this.rejectClipboard) { + this.rejectClipboard(); + } }; element.addEventListener("keyup", keyUpHandler);