11 changed files with 371 additions and 134 deletions
@ -0,0 +1,34 @@ |
|||
using System; |
|||
|
|||
using Avalonia; |
|||
using Avalonia.Platform; |
|||
using Avalonia.Web.Blazor; |
|||
|
|||
using ControlCatalog.Pages; |
|||
|
|||
using Microsoft.JSInterop; |
|||
|
|||
namespace ControlCatalog.Web; |
|||
|
|||
public class EmbedSampleWeb : INativeDemoControl |
|||
{ |
|||
public IPlatformHandle CreateControl(bool isSecond, IPlatformHandle parent, Func<IPlatformHandle> createDefault) |
|||
{ |
|||
var runtime = AvaloniaLocator.Current.GetRequiredService<IJSInProcessRuntime>(); |
|||
|
|||
if (isSecond) |
|||
{ |
|||
var iframe = runtime.Invoke<IJSInProcessObjectReference>("document.createElement", "iframe"); |
|||
iframe.InvokeVoid("setAttribute", "src", "https://www.youtube.com/embed/kZCIporjJ70"); |
|||
|
|||
return new JSObjectControlHandle(iframe); |
|||
} |
|||
else |
|||
{ |
|||
// window.createAppButton source is defined in "app.js" file.
|
|||
var button = runtime.Invoke<IJSInProcessObjectReference>("window.createAppButton"); |
|||
|
|||
return new JSObjectControlHandle(button); |
|||
} |
|||
} |
|||
} |
|||
@ -1,70 +0,0 @@ |
|||
.page { |
|||
position: relative; |
|||
display: flex; |
|||
flex-direction: column; |
|||
} |
|||
|
|||
.main { |
|||
flex: 1; |
|||
} |
|||
|
|||
.sidebar { |
|||
background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); |
|||
} |
|||
|
|||
.top-row { |
|||
background-color: #f7f7f7; |
|||
border-bottom: 1px solid #d6d5d5; |
|||
justify-content: flex-end; |
|||
height: 3.5rem; |
|||
display: flex; |
|||
align-items: center; |
|||
} |
|||
|
|||
.top-row ::deep a, .top-row .btn-link { |
|||
white-space: nowrap; |
|||
margin-left: 1.5rem; |
|||
} |
|||
|
|||
.top-row a:first-child { |
|||
overflow: hidden; |
|||
text-overflow: ellipsis; |
|||
} |
|||
|
|||
@media (max-width: 640.98px) { |
|||
.top-row:not(.auth) { |
|||
display: none; |
|||
} |
|||
|
|||
.top-row.auth { |
|||
justify-content: space-between; |
|||
} |
|||
|
|||
.top-row a, .top-row .btn-link { |
|||
margin-left: 0; |
|||
} |
|||
} |
|||
|
|||
@media (min-width: 641px) { |
|||
.page { |
|||
flex-direction: row; |
|||
} |
|||
|
|||
.sidebar { |
|||
width: 250px; |
|||
height: 100vh; |
|||
position: sticky; |
|||
top: 0; |
|||
} |
|||
|
|||
.top-row { |
|||
position: sticky; |
|||
top: 0; |
|||
z-index: 1; |
|||
} |
|||
|
|||
.main > div { |
|||
padding-left: 2rem !important; |
|||
padding-right: 1.5rem !important; |
|||
} |
|||
} |
|||
@ -1 +1,10 @@ |
|||
|
|||
window.createAppButton = function () { |
|||
var button = document.createElement('button'); |
|||
button.innerText = 'Hello world'; |
|||
var clickCount = 0; |
|||
button.onclick = () => { |
|||
clickCount++; |
|||
button.innerText = 'Click count ' + clickCount; |
|||
}; |
|||
return button; |
|||
} |
|||
|
|||
@ -0,0 +1,152 @@ |
|||
#nullable enable |
|||
using System.Diagnostics.CodeAnalysis; |
|||
|
|||
using Avalonia.Controls.Platform; |
|||
using Avalonia.Platform; |
|||
|
|||
using Microsoft.AspNetCore.Components; |
|||
using Microsoft.JSInterop; |
|||
|
|||
namespace Avalonia.Web.Blazor.Interop |
|||
{ |
|||
|
|||
internal class NativeControlHostInterop : JSModuleInterop, INativeControlHostImpl |
|||
{ |
|||
private const string JsFilename = "./_content/Avalonia.Web.Blazor/NativeControlHost.js"; |
|||
private const string CreateDefaultChildSymbol = "NativeControlHost.CreateDefaultChild"; |
|||
private const string CreateAttachmentSymbol = "NativeControlHost.CreateAttachment"; |
|||
private const string GetReferenceSymbol = "NativeControlHost.GetReference"; |
|||
|
|||
private readonly ElementReference hostElement; |
|||
|
|||
public static async Task<NativeControlHostInterop> ImportAsync(IJSRuntime js, ElementReference element) |
|||
{ |
|||
var interop = new NativeControlHostInterop(js, element); |
|||
await interop.ImportAsync(); |
|||
return interop; |
|||
} |
|||
|
|||
public NativeControlHostInterop(IJSRuntime js, ElementReference element) |
|||
: base(js, JsFilename) |
|||
{ |
|||
hostElement = element; |
|||
} |
|||
|
|||
public INativeControlHostDestroyableControlHandle CreateDefaultChild(IPlatformHandle parent) |
|||
{ |
|||
var element = Invoke<IJSInProcessObjectReference>(CreateDefaultChildSymbol); |
|||
return new JSObjectControlHandle(element); |
|||
} |
|||
|
|||
public INativeControlHostControlTopLevelAttachment CreateNewAttachment(Func<IPlatformHandle, IPlatformHandle> create) |
|||
{ |
|||
Attachment? a = null; |
|||
try |
|||
{ |
|||
using var hostElementJsReference = Invoke<IJSInProcessObjectReference>(GetReferenceSymbol, hostElement); |
|||
var child = create(new JSObjectControlHandle(hostElementJsReference)); |
|||
var attachmenetReference = Invoke<IJSInProcessObjectReference>(CreateAttachmentSymbol); |
|||
// It has to be assigned to the variable before property setter is called so we dispose it on exception
|
|||
#pragma warning disable IDE0017 // Simplify object initialization
|
|||
a = new Attachment(attachmenetReference, child); |
|||
#pragma warning restore IDE0017 // Simplify object initialization
|
|||
a.AttachedTo = this; |
|||
return a; |
|||
} |
|||
catch |
|||
{ |
|||
a?.Dispose(); |
|||
throw; |
|||
} |
|||
} |
|||
|
|||
public INativeControlHostControlTopLevelAttachment CreateNewAttachment(IPlatformHandle handle) |
|||
{ |
|||
var attachmenetReference = Invoke<IJSInProcessObjectReference>(CreateAttachmentSymbol); |
|||
var a = new Attachment(attachmenetReference, handle); |
|||
a.AttachedTo = this; |
|||
return a; |
|||
} |
|||
|
|||
public bool IsCompatibleWith(IPlatformHandle handle) => handle is JSObjectControlHandle; |
|||
|
|||
class Attachment : INativeControlHostControlTopLevelAttachment |
|||
{ |
|||
private const string InitializeWithChildHandleSymbol = "InitializeWithChildHandle"; |
|||
private const string AttachToSymbol = "AttachTo"; |
|||
private const string ShowInBoundsSymbol = "ShowInBounds"; |
|||
private const string HideWithSizeSymbol = "HideWithSize"; |
|||
private const string ReleaseChildSymbol = "ReleaseChild"; |
|||
|
|||
private IJSInProcessObjectReference? _native; |
|||
private NativeControlHostInterop? _attachedTo; |
|||
|
|||
public Attachment(IJSInProcessObjectReference native, IPlatformHandle handle) |
|||
{ |
|||
_native = native; |
|||
_native.InvokeVoid(InitializeWithChildHandleSymbol, ((JSObjectControlHandle)handle).Object); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (_native != null) |
|||
{ |
|||
_native.InvokeVoid(ReleaseChildSymbol); |
|||
_native.Dispose(); |
|||
_native = null; |
|||
} |
|||
} |
|||
|
|||
public INativeControlHostImpl? AttachedTo |
|||
{ |
|||
get => _attachedTo!; |
|||
set |
|||
{ |
|||
CheckDisposed(); |
|||
|
|||
var host = (NativeControlHostInterop?)value; |
|||
if (host == null) |
|||
{ |
|||
_native.InvokeVoid(AttachToSymbol); |
|||
} |
|||
else |
|||
{ |
|||
_native.InvokeVoid(AttachToSymbol, host.hostElement); |
|||
} |
|||
_attachedTo = host; |
|||
} |
|||
} |
|||
|
|||
public bool IsCompatibleWith(INativeControlHostImpl host) => host is NativeControlHostInterop; |
|||
|
|||
public void HideWithSize(Size size) |
|||
{ |
|||
CheckDisposed(); |
|||
if (_attachedTo == null) |
|||
return; |
|||
|
|||
_native.InvokeVoid(HideWithSizeSymbol, Math.Max(1, (float)size.Width), Math.Max(1, (float)size.Height)); |
|||
} |
|||
|
|||
public void ShowInBounds(Rect bounds) |
|||
{ |
|||
CheckDisposed(); |
|||
|
|||
if (_attachedTo == null) |
|||
throw new InvalidOperationException("Native control isn't attached to a toplevel"); |
|||
|
|||
bounds = new Rect(bounds.X, bounds.Y, Math.Max(1, bounds.Width), |
|||
Math.Max(1, bounds.Height)); |
|||
|
|||
_native.InvokeVoid(ShowInBoundsSymbol, (float)bounds.X, (float)bounds.Y, (float)bounds.Width, (float)bounds.Height); |
|||
} |
|||
|
|||
[MemberNotNull(nameof(_native))] |
|||
private void CheckDisposed() |
|||
{ |
|||
if (_native == null) |
|||
throw new ObjectDisposedException(nameof(Attachment)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,56 @@ |
|||
export class NativeControlHost { |
|||
public static CreateDefaultChild(parent: HTMLElement): HTMLElement { |
|||
return document.createElement("div"); |
|||
} |
|||
|
|||
// Used to convert ElementReference to JSObjectReference.
|
|||
// Is there a better way?
|
|||
public static GetReference(element: Element): Element { |
|||
return element; |
|||
} |
|||
|
|||
public static CreateAttachment(): NativeControlHostTopLevelAttachment { |
|||
return new NativeControlHostTopLevelAttachment(); |
|||
} |
|||
} |
|||
|
|||
class NativeControlHostTopLevelAttachment |
|||
{ |
|||
_child: HTMLElement; |
|||
_host: HTMLElement; |
|||
|
|||
InitializeWithChildHandle(child: HTMLElement) { |
|||
this._child = child; |
|||
this._child.style.position = "absolute"; |
|||
} |
|||
|
|||
AttachTo(host: HTMLElement): void { |
|||
if (this._host) { |
|||
this._host.removeChild(this._child); |
|||
} |
|||
|
|||
this._host = host; |
|||
|
|||
if (this._host) { |
|||
this._host.appendChild(this._child); |
|||
} |
|||
} |
|||
|
|||
ShowInBounds(x: number, y: number, width: number, height: number): void { |
|||
this._child.style.top = y + "px"; |
|||
this._child.style.left = x + "px"; |
|||
this._child.style.width = width + "px"; |
|||
this._child.style.height = height + "px"; |
|||
this._child.style.display = "block"; |
|||
} |
|||
|
|||
HideWithSize(width: number, height: number): void { |
|||
this._child.style.width = width + "px"; |
|||
this._child.style.height = height + "px"; |
|||
this._child.style.display = "none"; |
|||
} |
|||
|
|||
ReleaseChild(): void { |
|||
this._child = null; |
|||
} |
|||
} |
|||
@ -0,0 +1,35 @@ |
|||
#nullable enable |
|||
using Avalonia.Controls.Platform; |
|||
|
|||
using Microsoft.JSInterop; |
|||
|
|||
namespace Avalonia.Web.Blazor |
|||
{ |
|||
public class JSObjectControlHandle : INativeControlHostDestroyableControlHandle |
|||
{ |
|||
internal const string ElementReferenceDescriptor = "JSObjectReference"; |
|||
|
|||
public JSObjectControlHandle(IJSObjectReference reference) |
|||
{ |
|||
Object = reference; |
|||
} |
|||
|
|||
public IJSObjectReference Object { get; } |
|||
|
|||
public IntPtr Handle => throw new NotSupportedException(); |
|||
|
|||
public string? HandleDescriptor => ElementReferenceDescriptor; |
|||
|
|||
public void Destroy() |
|||
{ |
|||
if (Object is IJSInProcessObjectReference inProcess) |
|||
{ |
|||
inProcess.Dispose(); |
|||
} |
|||
else |
|||
{ |
|||
_ = Object.DisposeAsync(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue