From be943f30c5f1e61eddd6036623e032d418d663d6 Mon Sep 17 00:00:00 2001 From: Louie Colgan Date: Wed, 19 May 2021 09:25:42 +0100 Subject: [PATCH 1/7] feat(refactor): bring more in line with the api --- README.md | 59 +++- .../Pages/Index.razor | 8 +- .../Pages/LazyImages.razor | 14 +- .../Pages/Index.razor | 4 +- .../Pages/LazyImages.razor | 14 +- .../API/ForwardReference.cs | 20 ++ .../Configuration/Constants.cs | 12 +- ...Observe.base.cs => IntersectionObserve.cs} | 42 ++- .../IntersectionObserve.razor | 5 - .../IntersectionObserver.cs | 20 +- .../IntersectionObserverContext.cs | 11 + .../IntersectionObserverService.cs | 69 ++-- .../__tests__/index.test.ts | 308 +++++++++-------- .../package-lock.json | 69 +++- src/Blazor.IntersectionObserver/package.json | 3 +- .../rollup.config.js | 5 +- src/Blazor.IntersectionObserver/src/index.ts | 327 ++++++------------ src/Blazor.IntersectionObserver/tsconfig.json | 8 +- .../wwwroot/blazor-intersection-observer.js | 158 --------- .../blazor-intersection-observer.min.js | 1 + 20 files changed, 544 insertions(+), 613 deletions(-) create mode 100644 src/Blazor.IntersectionObserver/API/ForwardReference.cs rename src/Blazor.IntersectionObserver/{IntersectionObserve.base.cs => IntersectionObserve.cs} (58%) delete mode 100644 src/Blazor.IntersectionObserver/IntersectionObserve.razor create mode 100644 src/Blazor.IntersectionObserver/IntersectionObserverContext.cs delete mode 100644 src/Blazor.IntersectionObserver/wwwroot/blazor-intersection-observer.js create mode 100644 src/Blazor.IntersectionObserver/wwwroot/blazor-intersection-observer.min.js diff --git a/README.md b/README.md index fe527bd..9af3dae 100644 --- a/README.md +++ b/README.md @@ -60,8 +60,8 @@ object which contains the observer entry! Easy! @using Blazor.IntersectionObserver -
- Hey... look I'm @(context != null && context.IsIntersecting ? "in view": "out of view") +
+ Hey... look I'm @((context?.IsIntersecting ?? false) ? "in view": "out of view")
``` @@ -179,6 +179,37 @@ To disconnect the observer, call `Disconnect` on the `IntersectionObserver` inst observer.Disconnect(); ``` +This will remove all the observed elements from the observer, i.e. + +```razor +@using Blazor.IntersectionObserver +@implements IAsyncDisposable +@inject IIntersectionObserverService ObserverService + +
+ +@functions { + private IntersectionObserver Observer; + @* Code... *@ + + public async ValueTask DisconnectAll() + { + if (this.Observer != null) + { + await this.Observer.Disconnect(); + } + } +} + +``` + +#### `Dispose` +To remove the observer, call `Dispose` on the `IntersectionObserver` instance. + +```cs +observer.Dispose(); +``` + This is a useful method to clean up observers when components are disposed of, i.e. ```razor @@ -186,7 +217,7 @@ This is a useful method to clean up observers when components are disposed of, i @implements IAsyncDisposable @inject IIntersectionObserverService ObserverService -
+
@functions { private IntersectionObserver Observer; @@ -196,7 +227,7 @@ This is a useful method to clean up observers when components are disposed of, i { if (this.Observer != null) { - await this.Observer.Disconnect(); + await this.Observer.Dispose(); } } } @@ -209,12 +240,14 @@ This is a useful method to clean up observers when components are disposed of, i Rather than directly interfacing with the service, you can use this convenience component for quick and easy observing. You can access the observer entry through the implicit `@context`! +You need to make sure to provide the reference of the element you want to observe, this is done by passing the element reference to the context reference. + ```razor @* Injecting service... *@ -
- Hey... look I'm @(context != null && context.IsIntersecting ? "intersecting!": "not intersecting!") +
+ Hey... look I'm @((context?.IsIntersecting ?? false) ? "intersecting!": "not intersecting!")
@@ -227,13 +260,18 @@ Rather than directly interfacing with the service, you can use this convenience - `IsIntersecting` (`bool`) - Whether the element is intersecting - used for two-way binding. - `Options` (`IntersectionObserverOptions`) - The options for the observer. - `Once` (`bool`) - Only observe once for an intersection, then the instance disposes of itself. -- `Style` (`string`) - The style for the element. -- `Class` (`string`) - The class for the element. #### Context -The context is the `IntersectionObserverEntry` object, with the following signature: +The context is the `IntersectionObserverContext` object, with the following signature: ```cs +public class IntersectionObserverContext +{ + public IntersectionObserverEntry Entry { get; set; } + public ForwardReference Ref { get; set; } = new ForwardReference(); + public bool IsIntersecting => this.Entry?.IsIntersecting ?? false; +} + public class IntersectionObserverEntry { public bool IsIntersecting { get; set; } @@ -250,9 +288,6 @@ public class IntersectionObserverEntry } ``` -## Implementation Detail -To avoid creating an unnecessary number of observers for every element being observed, if a `Blazor Observer` shares exactly the same options as another, they will both use the same `IntersectionObserver` instance in JS. As each `Blazor Observer` has a unique id and callback, the elements that are being observed will still be passed to their respective `Blazor Observer`. - ## Feature Requests There's so much that `IntersectionObserver` can do, so if you have any requests or you want better documentation and examples, feel free to make a pull request or create an issue! diff --git a/samples/Blazor.IntersectionObserver.Client/Pages/Index.razor b/samples/Blazor.IntersectionObserver.Client/Pages/Index.razor index d959a2d..8ae89c4 100644 --- a/samples/Blazor.IntersectionObserver.Client/Pages/Index.razor +++ b/samples/Blazor.IntersectionObserver.Client/Pages/Index.razor @@ -15,9 +15,11 @@
- -
- Welcome to The Box! + +
+
+ Welcome to The Box! +
diff --git a/samples/Blazor.IntersectionObserver.Client/Pages/LazyImages.razor b/samples/Blazor.IntersectionObserver.Client/Pages/LazyImages.razor index c4cc1e3..00537d2 100644 --- a/samples/Blazor.IntersectionObserver.Client/Pages/LazyImages.razor +++ b/samples/Blazor.IntersectionObserver.Client/Pages/LazyImages.razor @@ -19,11 +19,13 @@
@foreach (var image in Images) { -
- - @{var isIntersecting = (context != null && context.IsIntersecting);} -
Loading...
- +
+ +
+ @{var isIntersecting = (context?.IsIntersecting ?? false);} +
Loading...
+ +
Card title
@@ -47,7 +49,7 @@ { var height = RandomNumber(250, 650); var width = RandomNumber(250, 650); - return $"https://www.placehold.it/{width}x{height}"; + return $"https://picsum.photos/{width}/{height}?id=${x}"; }) .ToList(); } diff --git a/samples/Blazor.IntersectionObserver.Server/Pages/Index.razor b/samples/Blazor.IntersectionObserver.Server/Pages/Index.razor index d959a2d..407f909 100644 --- a/samples/Blazor.IntersectionObserver.Server/Pages/Index.razor +++ b/samples/Blazor.IntersectionObserver.Server/Pages/Index.razor @@ -15,8 +15,8 @@
- -
+ +
Welcome to The Box!
diff --git a/samples/Blazor.IntersectionObserver.Server/Pages/LazyImages.razor b/samples/Blazor.IntersectionObserver.Server/Pages/LazyImages.razor index c4cc1e3..2225b5d 100644 --- a/samples/Blazor.IntersectionObserver.Server/Pages/LazyImages.razor +++ b/samples/Blazor.IntersectionObserver.Server/Pages/LazyImages.razor @@ -19,11 +19,13 @@
@foreach (var image in Images) { -
- - @{var isIntersecting = (context != null && context.IsIntersecting);} -
Loading...
- +
+ +
+ @{var isIntersecting = (context?.IsIntersecting ?? false);} +
Loading...
+ +
Card title
@@ -47,7 +49,7 @@ { var height = RandomNumber(250, 650); var width = RandomNumber(250, 650); - return $"https://www.placehold.it/{width}x{height}"; + return $"https://picsum.photos/{width}/{height}?id={x}"; }) .ToList(); } diff --git a/src/Blazor.IntersectionObserver/API/ForwardReference.cs b/src/Blazor.IntersectionObserver/API/ForwardReference.cs new file mode 100644 index 0000000..5969fa7 --- /dev/null +++ b/src/Blazor.IntersectionObserver/API/ForwardReference.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Components; + +namespace Blazor.IntersectionObserver.API +{ + public class ForwardReference + { + private ElementReference current; + + public ElementReference Current + { + get => this.current; + set => this.Set(value); + } + + public void Set(ElementReference value) + { + this.current = value; + } + } +} diff --git a/src/Blazor.IntersectionObserver/Configuration/Constants.cs b/src/Blazor.IntersectionObserver/Configuration/Constants.cs index 35f63b1..7f601fc 100644 --- a/src/Blazor.IntersectionObserver/Configuration/Constants.cs +++ b/src/Blazor.IntersectionObserver/Configuration/Constants.cs @@ -2,14 +2,16 @@ namespace Blazor.IntersectionObserver.Configuration { internal static class Constants { - public static string CREATE = $"create"; + public static string CREATE = "create"; - public static string OBSERVE = $"observe"; + public static string OBSERVE = "observe"; - public static string UNOBSERVE = $"unobserve"; + public static string UNOBSERVE = "unobserve"; - public static string DISCONNECT = $"disconnect"; + public static string DISCONNECT = "disconnect"; - public static string OBSERVE_ELEMENT = $"observeElement"; + public static string OBSERVE_ELEMENT = "observeElement"; + + public static string REMOVE = "remove"; } } diff --git a/src/Blazor.IntersectionObserver/IntersectionObserve.base.cs b/src/Blazor.IntersectionObserver/IntersectionObserve.cs similarity index 58% rename from src/Blazor.IntersectionObserver/IntersectionObserve.base.cs rename to src/Blazor.IntersectionObserver/IntersectionObserve.cs index dd5aa7f..e519f01 100644 --- a/src/Blazor.IntersectionObserver/IntersectionObserve.base.cs +++ b/src/Blazor.IntersectionObserver/IntersectionObserve.cs @@ -1,5 +1,6 @@ using Blazor.IntersectionObserver.API; using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Rendering; using System; using System.Collections.Generic; using System.Linq; @@ -7,15 +8,11 @@ namespace Blazor.IntersectionObserver.Components { - public class IntersectionObserveBase : ComponentBase, IAsyncDisposable + public class IntersectionObserve : ComponentBase, IAsyncDisposable { [Inject] private IIntersectionObserverService ObserverService { get; set; } - [Parameter] public string Class { get; set; } - - [Parameter] public string Style { get; set; } - - [Parameter] public RenderFragment ChildContent { get; set; } + [Parameter] public RenderFragment ChildContent { get; set; } [Parameter] public bool IsIntersecting { get; set; } @@ -23,13 +20,13 @@ public class IntersectionObserveBase : ComponentBase, IAsyncDisposable [Parameter] public EventCallback OnChange { get; set; } + [Parameter] public EventCallback OnDisposed { get; set; } + [Parameter] public IntersectionObserverOptions Options { get; set; } [Parameter] public bool Once { get; set; } - public ElementReference Element { get; set; } - - public IntersectionObserverEntry Entry { get; set; } + public IntersectionObserverContext IntersectionObserverContext { get; set; } = new IntersectionObserverContext(); private IntersectionObserver Observer { get; set; } @@ -43,7 +40,12 @@ protected override async Task OnAfterRenderAsync(bool firstRender) private async Task InitialiseObserver() { - this.Observer = await this.ObserverService.Observe(this.Element, this.OnIntersectUpdate, this.Options); + if (this.IntersectionObserverContext?.Ref?.Current == null) + { + throw new Exception("You need to provide the element to observe, for example: @ref=\"Context.Ref.Current\""); + } + + this.Observer = await this.ObserverService.Observe(this.IntersectionObserverContext.Ref.Current, this.OnIntersectUpdate, this.Options); } private async void OnIntersectUpdate(IList entries) @@ -55,22 +57,30 @@ private async void OnIntersectUpdate(IList entries) await this.IsIntersectingChanged.InvokeAsync(entry.IsIntersecting); await this.OnChange.InvokeAsync(entry); - this.Entry = entry; + this.IntersectionObserverContext.Entry = entry; this.StateHasChanged(); if (this.Once && entry.IsIntersecting) { - await this.Observer.Disconnect(); + await this.Observer.Dispose(); this.Observer = null; } } public async ValueTask DisposeAsync() { - if (this.Observer != null) - { - await this.Observer.Disconnect(); - } + var observer = this.Observer; + + if (observer == null) return; + + this.Observer = null; + await observer.Dispose(); + await this.OnDisposed.InvokeAsync(); + } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + this.ChildContent(this.IntersectionObserverContext)(builder); } } } diff --git a/src/Blazor.IntersectionObserver/IntersectionObserve.razor b/src/Blazor.IntersectionObserver/IntersectionObserve.razor deleted file mode 100644 index 77fa855..0000000 --- a/src/Blazor.IntersectionObserver/IntersectionObserve.razor +++ /dev/null @@ -1,5 +0,0 @@ -@inherits Blazor.IntersectionObserver.Components.IntersectionObserveBase - -
- @ChildContent(Entry) -
\ No newline at end of file diff --git a/src/Blazor.IntersectionObserver/IntersectionObserver.cs b/src/Blazor.IntersectionObserver/IntersectionObserver.cs index 007be89..0728634 100644 --- a/src/Blazor.IntersectionObserver/IntersectionObserver.cs +++ b/src/Blazor.IntersectionObserver/IntersectionObserver.cs @@ -33,7 +33,12 @@ public class IntersectionObserver /// /// On disconnecting the observer, trigger the action /// - private event Func OnDisconnect; + private event Func> OnDisconnect; + + /// + /// On disconnecting the observer, trigger the action + /// + private event Func OnRemove; /// /// Initialise the intersection observer with the @@ -48,7 +53,8 @@ public IntersectionObserver( Action> onIntersectUpdate, Func onObserve, Func onUnobserve, - Func onDisconnect + Func> onDisconnect, + Func onRemove ) { this.Id = id; @@ -56,6 +62,7 @@ Func onDisconnect OnObserve = onObserve; OnUnobserve = onUnobserve; OnDisconnect = onDisconnect; + OnRemove = onRemove; } /// @@ -99,5 +106,14 @@ public async ValueTask Disconnect() { await (OnDisconnect.Invoke(this.Id)); } + + /// + /// Signal that the observer should be + /// disposed. + /// + public async ValueTask Dispose() + { + await OnRemove.Invoke(this.Id); + } } } diff --git a/src/Blazor.IntersectionObserver/IntersectionObserverContext.cs b/src/Blazor.IntersectionObserver/IntersectionObserverContext.cs new file mode 100644 index 0000000..83d47a2 --- /dev/null +++ b/src/Blazor.IntersectionObserver/IntersectionObserverContext.cs @@ -0,0 +1,11 @@ +using Blazor.IntersectionObserver.API; + +namespace Blazor.IntersectionObserver +{ + public class IntersectionObserverContext + { + public IntersectionObserverEntry Entry { get; set; } + public ForwardReference Ref { get; set; } = new ForwardReference(); + public bool IsIntersecting => this.Entry?.IsIntersecting ?? false; + } +} diff --git a/src/Blazor.IntersectionObserver/IntersectionObserverService.cs b/src/Blazor.IntersectionObserver/IntersectionObserverService.cs index 8a254f1..0c0b54d 100644 --- a/src/Blazor.IntersectionObserver/IntersectionObserverService.cs +++ b/src/Blazor.IntersectionObserver/IntersectionObserverService.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Components; using Microsoft.JSInterop; using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Threading.Tasks; @@ -10,6 +11,8 @@ namespace Blazor.IntersectionObserver { public class IntersectionObserverService: IIntersectionObserverService, IAsyncDisposable { + private readonly string scriptPath = "/_content/BlazorIntersectionObserver/blazor-intersection-observer.min.js"; + private readonly Task moduleTask; private DotNetObjectReference objectRef; @@ -17,12 +20,11 @@ public class IntersectionObserverService: IIntersectionObserverService, IAsyncDi /// /// Contains a reference of observer instances and their ids. /// - private readonly IDictionary observers = new Dictionary(); - + private readonly ConcurrentDictionary observers = new ConcurrentDictionary(); public IntersectionObserverService(IJSRuntime jsRuntime) { - this.moduleTask = jsRuntime.InvokeAsync("import", "/_content/BlazorIntersectionObserver/blazor-intersection-observer.js").AsTask(); + this.moduleTask = jsRuntime.InvokeAsync("import", this.scriptPath).AsTask(); this.objectRef = DotNetObjectReference.Create(this); } @@ -79,7 +81,12 @@ public async Task Observe( [JSInvokable(nameof(OnCallback))] public void OnCallback(string id, IList entries) { - this.observers[id]?.OnIntersect(entries); + this.EnsureObserverExists(id); + + if (this.observers.TryGetValue(id, out var observer)) + { + observer.OnIntersect(entries); + } } /// @@ -90,11 +97,21 @@ public void OnCallback(string id, IList entries) /// The observer instance private IntersectionObserver CreateObserver(string observerId, Action> onIntersectUpdate) { - var observer = new IntersectionObserver(observerId, onIntersectUpdate, this.ObserveElement, this.Unobserve, this.Disconnect); - - this.observers.Add(observerId, observer); + var observer = new IntersectionObserver( + observerId, + onIntersectUpdate, + this.ObserveElement, + this.Unobserve, + this.Disconnect, + this.RemoveObserver + ); + + if (this.observers.TryAdd(observerId, observer)) + { + return observer; + } - return observer; + throw new Exception($"Failed to create observer for key: {observerId}"); } /// @@ -121,27 +138,41 @@ private async ValueTask Unobserve(string id, ElementReference element) { var module = await this.moduleTask; - var unobserved = await module.InvokeAsync(Constants.UNOBSERVE, id, element); + await module.InvokeAsync(Constants.UNOBSERVE, id, element); + } - if (unobserved) - { - this.observers.Remove(id); - } + /// + /// Disconnect the observer instance + /// + /// The observer instance id + private async ValueTask Disconnect(string id) + { + var module = await this.moduleTask; + + return await module.InvokeAsync(Constants.DISCONNECT, id); } /// /// Disconnect the observer instance /// /// The observer instance id - private async ValueTask Disconnect(string id) + private async ValueTask RemoveObserver(string id) { var module = await this.moduleTask; - var disconnected = await module.InvokeAsync(Constants.DISCONNECT, id); + var disposed = await module.InvokeAsync(Constants.REMOVE, id); + + if (disposed) + { + this.observers.TryRemove(id, out _); + } + } - if (disconnected) + private void EnsureObserverExists(string id) + { + if (!this.observers.ContainsKey(id)) { - this.observers.Remove(id); + throw new Exception($"There is no observer for key: {id}"); } } @@ -149,9 +180,7 @@ public async ValueTask DisposeAsync() { this.objectRef?.Dispose(); - var module = await this.moduleTask; - - await module.DisposeAsync(); + await Task.CompletedTask; } } } diff --git a/src/Blazor.IntersectionObserver/__tests__/index.test.ts b/src/Blazor.IntersectionObserver/__tests__/index.test.ts index dc3fd1f..bed49e2 100644 --- a/src/Blazor.IntersectionObserver/__tests__/index.test.ts +++ b/src/Blazor.IntersectionObserver/__tests__/index.test.ts @@ -1,8 +1,5 @@ import * as ObserverJS from "../src/index"; -import { getMockElement } from "./utils/document"; -import { getSetValueFirstOrDefault } from "./utils/iterable"; import { getObserverEntry } from "./data/index"; -import { getObserverItemId } from "./utils/config"; declare var window: any; @@ -10,202 +7,209 @@ const observe = jest.fn(); const unobserve = jest.fn(); const disconnect = jest.fn(); let onEntryChangeCallback: ObserverJS.OnIntersectionUpdateFn | null; +let observerOptions: IntersectionObserverInit | null; const mockDotNetRef = { - invokeMethodAsync: jest.fn() + invokeMethodAsync: jest.fn(), }; -window.IntersectionObserver = jest.fn(function(fn) { - onEntryChangeCallback = fn; - return { - observe, - unobserve, - disconnect - }; +window.IntersectionObserver = jest.fn(function (fn, options) { + onEntryChangeCallback = fn; + observerOptions = options; + return { + observe, + unobserve, + disconnect, + }; }); beforeEach(() => { - ObserverJS.reset(); - observe.mockReset(); - unobserve.mockReset(); - disconnect.mockReset(); - mockDotNetRef.invokeMethodAsync.mockReset(); - onEntryChangeCallback = null; + ObserverJS.reset(); + observe.mockReset(); + unobserve.mockReset(); + disconnect.mockReset(); + mockDotNetRef.invokeMethodAsync.mockReset(); + onEntryChangeCallback = null; + observerOptions = null; }); describe("when creating an observer", () => { + it("should create a new observer item", () => { + const callbackId = "1"; - test("should create a new observer instance to a observer item", () => { - const observerId = "1"; - const response = ObserverJS.create(mockDotNetRef, observerId, {}); - const elements = response.instance.elements.get(observerId); - const instances = ObserverJS.getObserverItems(); - - expect(instances.size).toBe(1); - expect(elements).toBeDefined(); - !!elements && expect(elements.size).toBe(0); - }); - - test("should create new observer instances to a single observer item", () => { - const observerIds = ["1", "2"]; - - observerIds.forEach((id) => { - ObserverJS.create(mockDotNetRef, id, {}); - }); - - const items = ObserverJS.getObserverItems(); - const instance = items.get(getObserverItemId(observerIds[0])); - - expect(items.size).toBe(1); - expect(instance).toBeDefined(); - !!instance && expect(instance.elements.size).toBe(2); - }); - - test("should create new observer instances to multiple observer items", () => { - const observers: Array<{ id: string, options: IntersectionObserverInit }> = [ - { id: "1", options: { rootMargin: "10px" } }, - { id: "2", options: { rootMargin: "11px" } } - ]; - - observers.forEach(({ id, options }) => { - ObserverJS.create(mockDotNetRef, id, options); - }); - - const items = ObserverJS.getObserverItems(); - - expect(items.size).toBe(2); - }); - - test("should create a new observer instance with elements", () => { - const observerId = "1"; - const mockDiv = getMockElement("div"); - const mockSpan = getMockElement("span"); - - const item = ObserverJS.create(mockDotNetRef, observerId, {}); - - ObserverJS.observeElement(observerId, mockDiv); - ObserverJS.observeElement(observerId, mockDiv); - ObserverJS.observeElement(observerId, mockSpan); - const elements = item.instance.elements.get(observerId) - - expect(elements).toBeDefined(); - expect(observe).toBeCalledTimes(2); - !!elements && expect(elements.has(mockDiv)).toBeTruthy(); - !!elements && expect(elements.has(mockSpan)).toBeTruthy(); - }); + ObserverJS.create(mockDotNetRef, callbackId); + const items = ObserverJS.getObserverItems(); + + expect(items.size).toBe(1); + }); + + it("should create multiple observer items", () => { + const callbackIds = ["1", "2"]; + + callbackIds.forEach((id) => ObserverJS.create(mockDotNetRef, id)); + + const items = ObserverJS.getObserverItems(); + + expect(items.size).toBe(2); + }); + + it("should create an observer item and observe elements", () => { + const callbackId = "1"; + + ObserverJS.create(mockDotNetRef, callbackId); + + const mockDiv = document.createElement("div"); + const mockSpan = document.createElement("span"); + + const observeElementId1 = ObserverJS.observeElement(callbackId, mockDiv); + const observeElementId2 = ObserverJS.observeElement(callbackId, mockSpan); + + expect(observe).toBeCalledTimes(2); + expect(observeElementId1).toBe(`${ObserverJS.OBSERVER_ID_PREFIX}0`); + expect(observeElementId2).toBe(`${ObserverJS.OBSERVER_ID_PREFIX}1`); + }); }); describe("when observing an element", () => { + it("should create an observer item and immediately observe the element", () => { + const callbackId = "1"; + const mockDiv = document.createElement("div"); - it("should create an observer instance and immediately observe the element", () => { - const observerId = "1"; - const mockDiv = getMockElement("div"); + const observeElementId = ObserverJS.observe( + mockDotNetRef, + callbackId, + mockDiv + ); - ObserverJS.observe(mockDotNetRef, observerId, mockDiv, {}); + const items = ObserverJS.getObserverItems(); - const items = ObserverJS.getObserverItems(); + expect(items.size).toBe(1); + expect(observe).toBeCalledTimes(1); + expect(observeElementId).toBe(`${ObserverJS.OBSERVER_ID_PREFIX}0`); + }); - expect(items.size).toBe(1); - expect(observe).toBeCalledTimes(1); - }); + it("should observe the element within a threshold", () => { + const callbackId = "1"; + const mockDiv = document.createElement("div"); + const definedOptions = { threshold: 0.5 }; - it("should create one observer instance and observe elements", () => { - const observerId = "1"; - const observers: Array<{ id: string, el: HTMLElement, options: IntersectionObserverInit }> = [ - { id: observerId, el: document.createElement("div"), options: { rootMargin: "10px" } }, - { id: observerId, el: document.createElement("span"), options: { rootMargin: "10px" } } - ]; + ObserverJS.observe(mockDotNetRef, callbackId, mockDiv, definedOptions); - const instanceRef: ObserverJS.IntersectionObserverInstance[] = []; + const items = ObserverJS.getObserverItems(); - observers.forEach(({ id, el, options }) => { - const ref = ObserverJS.observe(mockDotNetRef, id, el, options); - instanceRef.push(ref); - }); + expect(items.size).toBe(1); + expect(observe).toBeCalledTimes(1); + expect(observerOptions).toBe(definedOptions); + }); - const items = ObserverJS.getObserverItems(); - const [lastInstance] = instanceRef; - const lastInstanceElements = lastInstance.elements.get(observerId); - - expect(items.size).toBe(1); - lastInstanceElements && expect(lastInstanceElements.size).toBe(2); - }); + it("should throw an error if the observer item does not exist", () => { + const callbackId = "1"; + const mockDiv = document.createElement("div"); + expect(() => { + ObserverJS.observeElement(callbackId, mockDiv); + }).toThrowError(); + }); }); describe("when unobserving an element", () => { + it("should unobserve an element for an observer item", () => { + const callbackId = "1"; + const mockDiv = document.createElement("div"); + + ObserverJS.observe(mockDotNetRef, callbackId, mockDiv); + const removedElementId = ObserverJS.unobserve(callbackId, mockDiv); + + const items = ObserverJS.getObserverItems(); + const instance = items.get(callbackId); + + expect(unobserve).toHaveBeenCalledTimes(1); + expect(items.size).toBe(1); + expect(instance).toBeDefined(); + expect(removedElementId).toBe(`${ObserverJS.OBSERVER_ID_PREFIX}0`); + }); + + it("should throw an error if the observer item does not exist", () => { + const callbackId = "1"; + const mockDiv = document.createElement("div"); + + expect(() => { + ObserverJS.unobserve(callbackId, mockDiv); + }).toThrowError(); + }); +}); - it("should create an observer instance and unobserve an element", () => { - const observerId = "1"; - const mockDiv = getMockElement("div"); +describe("when disconnecting an observer", () => { + it("should disconnect an observer for observer item", () => { + const callbackId = "1"; + const mockDiv = document.createElement("div"); - ObserverJS.observe(mockDotNetRef, observerId, mockDiv, {}); - const removed = ObserverJS.unobserve(observerId, mockDiv); + ObserverJS.observe(mockDotNetRef, callbackId, mockDiv); + ObserverJS.disconnect(callbackId); - const items = ObserverJS.getObserverItems(); - const instance = items.get(getObserverItemId(observerId)); + expect(disconnect).toHaveBeenCalledTimes(1); + }); - expect(unobserve).toHaveBeenCalledTimes(1); - expect(removed).toBeTruthy(); - expect(items.size).toBe(1); - expect(instance).toBeDefined(); - !!instance && expect(getSetValueFirstOrDefault(instance.elements).size).toBe(0); - }); + it("should throw an error if the observer item does not exist", () => { + const callbackId = "1"; + expect(() => { + ObserverJS.disconnect(callbackId); + }).toThrowError(); + }); }); -describe("when disconnecting an observer", () => { +describe("when removing an observer item", () => { + it("should remove the observer item from the observer items", () => { + const callbackId = "1"; + const mockDiv = document.createElement("div"); - it("should disconnect an observer", () => { - const observerId = "1"; - const mockDiv = getMockElement("div"); - - ObserverJS.observe(mockDotNetRef, observerId, mockDiv, {}); - ObserverJS.disconnect(observerId); + ObserverJS.observe(mockDotNetRef, callbackId, mockDiv); + ObserverJS.remove(callbackId); - const items = ObserverJS.getObserverItems(); + const items = ObserverJS.getObserverItems(); - expect(disconnect).toHaveBeenCalledTimes(1); - expect(items.size).toBe(0); - }); + expect(disconnect).toHaveBeenCalledTimes(1); + expect(items.size).toBe(0); + }); + it("should throw an error if the observer item does not exist", () => { + const callbackId = "1"; + + expect(() => { + ObserverJS.remove(callbackId); + }).toThrowError(); + }); }); describe("when an element is intersecting", () => { + it("should return a list of entries to the observer instance dotnet reference", () => { + const callbackId = "1"; + const mockDiv = document.createElement("div"); + + ObserverJS.observe(mockDotNetRef, callbackId, mockDiv); - it("should return a list of entries to the relevant observer instance", () => { - const observerId = "1"; - const mockDiv = getMockElement("div"); - - ObserverJS.observe(mockDotNetRef, observerId, mockDiv, {}); + const entry = getObserverEntry({ target: mockDiv }); - if (onEntryChangeCallback != null) { - onEntryChangeCallback([ - getObserverEntry({ target: mockDiv }) - ]); - } - - const [[arg1, arg2, arg3]] = mockDotNetRef.invokeMethodAsync.mock.calls; + onEntryChangeCallback!([entry]); - expect(arg1).toBe("OnCallback"); - expect(arg2).toBe(observerId); - }); + const [[arg1, arg2, [passedEntry]]] = + mockDotNetRef.invokeMethodAsync.mock.calls; - it("should not return a list of entries if there are no observer instances", () => { - const observerId = "1"; - const mockDiv = getMockElement("div"); + expect(arg1).toBe("OnCallback"); + expect(arg2).toBe(callbackId); + }); - ObserverJS.observe(mockDotNetRef, observerId, mockDiv, {}); - ObserverJS.disconnect(observerId); + it("should not return a list of entries if the observer item does not exist", () => { + const callbackId = "1"; + const mockDiv = document.createElement("div"); - if (onEntryChangeCallback != null) { - onEntryChangeCallback([ - getObserverEntry({ target: mockDiv }) - ]); - } + ObserverJS.observe(mockDotNetRef, callbackId, mockDiv); + ObserverJS.remove(callbackId); - expect(mockDotNetRef.invokeMethodAsync).toHaveBeenCalledTimes(0); - }); + onEntryChangeCallback!([getObserverEntry()]); + expect(mockDotNetRef.invokeMethodAsync).toHaveBeenCalledTimes(0); + }); }); diff --git a/src/Blazor.IntersectionObserver/package-lock.json b/src/Blazor.IntersectionObserver/package-lock.json index 307e4a1..5427a09 100644 --- a/src/Blazor.IntersectionObserver/package-lock.json +++ b/src/Blazor.IntersectionObserver/package-lock.json @@ -1,6 +1,6 @@ { "name": "blazor.intersectionobserver", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1294,6 +1294,12 @@ "delayed-stream": "~1.0.0" } }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -2026,9 +2032,9 @@ } }, "hosted-git-info": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, "html-encoding-sniffer": { @@ -3068,9 +3074,9 @@ } }, "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, "lodash.memoize": { @@ -3599,6 +3605,15 @@ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", "dev": true }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, "react-is": { "version": "17.0.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz", @@ -3818,6 +3833,18 @@ } } }, + "rollup-plugin-terser": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz", + "integrity": "sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "jest-worker": "^26.2.1", + "serialize-javascript": "^4.0.0", + "terser": "^5.0.0" + } + }, "rsvp": { "version": "4.8.5", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", @@ -4001,6 +4028,15 @@ "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", "dev": true }, + "serialize-javascript": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", + "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + }, "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -4424,6 +4460,25 @@ "supports-hyperlinks": "^2.0.0" } }, + "terser": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.7.0.tgz", + "integrity": "sha512-HP5/9hp2UaZt5fYkuhNBR8YyRcT8juw8+uFbAme53iN9hblvKnLUTKkmwJG6ocWpIKf8UK4DoeWG4ty0J6S6/g==", + "dev": true, + "requires": { + "commander": "^2.20.0", + "source-map": "~0.7.2", + "source-map-support": "~0.5.19" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + } + } + }, "test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", diff --git a/src/Blazor.IntersectionObserver/package.json b/src/Blazor.IntersectionObserver/package.json index b5a9576..ca05863 100644 --- a/src/Blazor.IntersectionObserver/package.json +++ b/src/Blazor.IntersectionObserver/package.json @@ -16,8 +16,9 @@ "@types/jest": "26.0.15", "jest": "26.6.3", "jest-junit": "12.0.0", - "ts-jest": "26.4.4", "rollup": "^2.34.0", + "rollup-plugin-terser": "^7.0.2", + "ts-jest": "26.4.4", "tslib": "^2.0.3", "typescript": "4.1.2" } diff --git a/src/Blazor.IntersectionObserver/rollup.config.js b/src/Blazor.IntersectionObserver/rollup.config.js index aab1f21..77a2559 100644 --- a/src/Blazor.IntersectionObserver/rollup.config.js +++ b/src/Blazor.IntersectionObserver/rollup.config.js @@ -1,13 +1,14 @@ import typescript from '@rollup/plugin-typescript'; +import { terser } from "rollup-plugin-terser"; export default { input: 'src/index.ts', output: { - file: 'wwwroot/blazor-intersection-observer.js', + file: 'wwwroot/blazor-intersection-observer.min.js', format: 'es', sourcemap: false, }, plugins: [typescript({ sourceMap: false, - })], + }), terser()], }; diff --git a/src/Blazor.IntersectionObserver/src/index.ts b/src/Blazor.IntersectionObserver/src/index.ts index 3591e12..225bd69 100644 --- a/src/Blazor.IntersectionObserver/src/index.ts +++ b/src/Blazor.IntersectionObserver/src/index.ts @@ -1,21 +1,16 @@ -export interface IDotNetObjectRef { +export interface DotNetObjectRef { invokeMethodAsync(methodName: string, ...args: any[]): Promise; } -export interface IntersectionObserverItem { - id: string; - instance: IntersectionObserverInstance; +export interface IntersectionObserverItemElement { + element: Element; + elementId: string; } -export interface IntersectionObserverInstance { - dotnetRef: IDotNetObjectRef; +export interface IntersectionObserverItem { + dotnetRef: DotNetObjectRef; observer: IntersectionObserver; - options: IntersectionObserverInit; - elements: Map>; -} - -export interface ElementInstance { - observers: IntersectionObserverInstance[]; + elements: IntersectionObserverItemElement[]; } export type OnIntersectionUpdateFn = ( @@ -26,13 +21,13 @@ export const OBSERVER_ID_PREFIX = "blazor_plugin_observer__"; let OBSERVER_ID = 1; -const observerItems = new Map(); +const observerItems = new Map(); /** * Reset the counter and the observer instances */ export function reset() { - OBSERVER_ID = 1; + OBSERVER_ID = 0; observerItems.clear(); } @@ -46,232 +41,146 @@ export function getObserverItems() { /** * Generate a unique id for an observer item. **/ -function createObserverItemId() { +function getObserverElementId() { return `${OBSERVER_ID_PREFIX}${OBSERVER_ID++}`; } /** - * Check whether the two options are the same. - * @param {IntersectionObserverInit} a - An observer's options to compare - * @param {IntersectionObserverInit} b - An observer's options to compare - * @returns {boolean} - Whether the two options are the same + * Create a new intersection observer item. + * @param {DotNetObjectRef} dotnetRef - The current dotnet blazor reference + * @param {string} callbackId - The callback id for the blazor observer service + * @param {globalThis.IntersectionObserverInit} options - The intersection options + * @returns {IntersectionObserverItem} - The resize observer item */ -function hasSameOptions( - a: IntersectionObserverInit, - b: IntersectionObserverInit -): boolean { - return ( - a.root === b.root && - a.rootMargin === b.rootMargin && - a.threshold === b.threshold - ); -} +function createObserverItem( + dotnetRef: DotNetObjectRef, + callbackId: string, + options?: globalThis.IntersectionObserverInit +): IntersectionObserverItem { + const onEntry = onEntryChange(callbackId); -/** - * If there's an existing observer item, retrieve it given the same options. - * @param {IntersectionObserverInit} options - The observer options - * @returns {IntersectionObserverItem | null} - The observer item or nothing - */ -function getItemFromOptions( - options: IntersectionObserverInit -): IntersectionObserverItem | null { - let itemFound: IntersectionObserverItem | null = null; - - for (const [id, instance] of observerItems) { - if (hasSameOptions(options, instance.options)) { - itemFound = { id, instance }; - break; - } - } + const observer = new IntersectionObserver(onEntry, options); - return itemFound; -} + observerItems.set(callbackId, { dotnetRef, observer, elements: [] }); -/** - * Add an element to the observer instance. - * @param {IntersectionObserverInstance} instance - The observer instance - * @param {Element} element - The element to add to the instance - * @param {string} observerId - The observer id - * @returns {IntersectionObserverInstance} - The observer instance - */ -function observeInstanceElement( - instance: IntersectionObserverInstance, - observerId: string, - element: Element -) { - const { elements, observer } = instance; - const observerElements = elements.get(observerId); - - if (observerElements != null) { - observerElements.add(element); - } else { - elements.set( - observerId, - new Set([element]) - ); - } - - observer.observe(element); - return instance; + return observerItems.get(callbackId)!; } /** - * Create or use an existing intersection observer item. - * @param {DotNetObjectRef} dotnetRef - The current dotnet blazor reference - * @param {IntersectionObserverEntryInit} options - The observer options - * @returns {IntersectionObserverItem} - The intersection observer item + * Observe an element for the observer item + * @param {string} callbackId - The callback id for the blazor observer service + * @param {Element} element - The element to observe + * @returns {string} - The observer element id */ -function getObserverItem( - dotnetRef: IDotNetObjectRef, - options: IntersectionObserverInit -): IntersectionObserverItem { - if (options == null) { - options = {}; +export function observeElement(callbackId: string, element: Element): string { + const item = observerItems.get(callbackId); + + if (item == null) { + throw new Error(`Failed to observe element for key: ${callbackId} as the observer does not exist`); } - const observerItem = getItemFromOptions(options); + if (item.elements.some(record => record.element == element)) { + console.warn(`BlazorIntersectionObserver: The element is already being observed by observer for key ${callbackId}`); + return ""; + } - if (observerItem == null) { - const id = createObserverItemId(); - const observer = new IntersectionObserver(onEntryChange(id), options); - const elements = new Map>(); + const elementId = getObserverElementId(); - observerItems.set(id, { dotnetRef, options, observer, elements }); + item.observer.observe(element); + item.elements.push({ elementId, element }); - return { - id, - instance: observerItems.get(id) as IntersectionObserverInstance, - }; - } - - return observerItem; + return elementId; } /** * Create a intersection observer. * @param {IDotNetObjectRef} dotnetRef - The dotnet interop reference - * @param {string} id - The instance id of the "observer" - * @param {IntersectionObserverInit} options - The intersection obsever options - * @returns {IntersectionObserverItem} - The observer item + * @param {string} callbackId - The callback id for the blazor observer service + * @param {globalThis.IntersectionObserverInit} options - The intersection observer options + * @returns {ResizeObserverItem} - The observer item */ export function create( - dotnetRef: IDotNetObjectRef, - id: string, - options: IntersectionObserverInit + dotnetRef: DotNetObjectRef, + callbackId: string, + options?: globalThis.IntersectionObserverInit ) { - const item = getObserverItem(dotnetRef, options); - const { instance } = item; - instance.elements.set(id, new Set([])); - return item; + return createObserverItem(dotnetRef, callbackId, options); } /** - * Observe the target node using a new or existing observer - * @param {IDotNetObjectRef} dotnetRef - The dotnet interop reference - * @param {string} id - The instance id of the "observer" + * Observe the target node using a new observer + * @param {DotNetObjectRef} dotnetRef - The dotnet interop reference + * @param {string} callbackId - The callback id for the blazor observer service * @param {Element} node - The node to observe - * @param {IntersectionObserverInit} options - The intersection observer options - * @returns {IntersectionObserverInstance} - The observer instance + * @param {globalThis.IntersectionObserverInit} options - The intersection observer options + * @returns {string} - The observer element id */ export function observe( - dotnetRef: IDotNetObjectRef, - id: string, + dotnetRef: DotNetObjectRef, + callbackId: string, node: Element, - options: IntersectionObserverInit -) { - const { instance } = getObserverItem(dotnetRef, options); - return observeInstanceElement(instance, id, node); + options?: globalThis.IntersectionObserverInit +): string { + console.log({ dotnetRef, callbackId, node }) + createObserverItem(dotnetRef, callbackId, options); + return observeElement(callbackId, node); } /** - * Observe an element for the observer instance. - * @param id - The observer id - * @param element - The element to observe + * Unobserve the element for the observer item. + * @param {string} id - The observer item id + * @param {Element} element - The element to unobserve + * @returns {boolean} - Whether the element has been unobserved */ -export function observeElement(id: string, element: Element) { - const instances = observerItems.values(); - - for (const instance of instances) { - const elements = instance.elements.get(id); +export function unobserve(callbackId: string, element: Element): string { + const item = observerItems.get(callbackId); - if (elements != null && !elements.has(element)) { - instance.observer.observe(element); - elements.add(element); - break; - } + if (item == null) { + throw new Error(`Failed to unobserve element for key: ${callbackId} as the observer does not exist`); } -} -/** - * If there are no elements in the observer - * instance, disconnect the observer and remove - * it from the observer instances. - * @param {string} itemId - The observer item id - * @param {IntersectionObserverInstance} instance - The instance to remove - * @returns {boolean} - Whether the instance has been removed - */ -function cleanupObserver( - itemId: string, - instance: IntersectionObserverInstance -) { - let instanceRemoved = false; + const unobserveElementId = item.elements.find((record) => record.element == element)?.elementId; - if (instance.elements.size === 0) { - instance.observer.disconnect(); - observerItems.delete(itemId); - instanceRemoved = true; + if (unobserveElementId == null) { + console.warn(`BlazorResizeObserver: The record does not exist for observer: ${callbackId}`); } - return instanceRemoved; -} + item.observer.unobserve(element); + item.elements = item.elements.filter((record) => record.element != element); + return unobserveElementId!; +} /** - * Remove the element from the observer instance and - * unobserve the element. - * @param {string} id - The observer id - * @param {Element} element - The node to unobserve - * @returns {boolean} - Whether the element has been unobserved + * Disconnect the observered elements from the observer item. + * @param {string} callbackId - The observer item id + * @returns {boolean} - Whether the elements have + * been removed from the observer item */ -export function unobserve(id: string, element: Element) { - const instances = observerItems.values(); - let removed = false; - - for (const instance of instances) { - const elements = instance.elements.get(id); - - if (elements != null && elements.has(element)) { - instance.observer.unobserve(element); - elements.delete(element); - removed = true; - break; - } +export function disconnect(callbackId: string): boolean { + const item = observerItems.get(callbackId); + + if (item == null) { + throw new Error(`Failed to disconnect for key: ${callbackId} as the observer does not exist`); } - return removed; + item.observer.disconnect(); + item.elements = []; + + return true; } /** - * Delete the elements from the observer instance - * and trigger cleaning up the observers. - * @param {string} id - The observer instance id - * @returns {boolean} - Whether the elements have - * been removed from the observer instance + * Remove the observer item. + * @param {string} callbackId - The observer item id + * @returns {boolean} - Whether the observer item has been + * removed. */ -export function disconnect(id: string) { - const observers = observerItems.entries(); - let disconnected = false; - - for (const [instanceId, instance] of observers) { - if (instance.elements.get(id)) { - instance.elements.delete(id); - cleanupObserver(instanceId, instance); - disconnected = true; - break; - } +export function remove(callbackId: string): boolean { + if (disconnect(callbackId)) { + return observerItems.delete(callbackId); } - - return disconnected; + return false; } /** @@ -305,36 +214,28 @@ function toEntryObject(entry: IntersectionObserverEntry) { /** * Returns a function that will be triggered when an - * element has an intersection update. - * @param {string} observerInstanceId - The instance id - * @returns {OnIntersectionUpdateFn} - The function triggered by an intersection update + * element has an resize update. + * @param {string} callbackId - The observer item id + * @returns {OnIntersectionUpdateFn} - The function triggered by an resize update */ -function onEntryChange(observerInstanceId: string): OnIntersectionUpdateFn { - return (entries: IntersectionObserverEntry[]) => { - if (!observerItems.has(observerInstanceId)) { +function onEntryChange(callbackId: string): OnIntersectionUpdateFn { + return (entries: readonly IntersectionObserverEntry[]) => { + + if (!observerItems.has(callbackId)) { return; } - const { dotnetRef, elements } = observerItems.get( - observerInstanceId - ) as IntersectionObserverInstance; - - const observerEntries = entries.reduce((batched, entry) => { - for (const [id, item] of elements) { - if (item.has(entry.target)) { - batched[id] = (batched[id] || []).concat(entry); - } - } - return batched; - }, {}); - - Object.keys(observerEntries).forEach((observerId) => { - const batch = observerEntries[observerId] as IntersectionObserverEntry[]; - dotnetRef.invokeMethodAsync( - "OnCallback", - observerId, - batch.map(toEntryObject) - ); + const { dotnetRef } = observerItems.get(callbackId)!; + + const mapped = entries.map((entry) => { + const mappedEntry = toEntryObject(entry); + return mappedEntry; }); + + dotnetRef.invokeMethodAsync( + "OnCallback", + callbackId, + mapped + ); }; } diff --git a/src/Blazor.IntersectionObserver/tsconfig.json b/src/Blazor.IntersectionObserver/tsconfig.json index 68fb26d..175cce7 100644 --- a/src/Blazor.IntersectionObserver/tsconfig.json +++ b/src/Blazor.IntersectionObserver/tsconfig.json @@ -4,12 +4,14 @@ "noEmitOnError": true, "removeComments": true, "sourceMap": true, - "target": "esnext", + "target": "ES2019", "module": "esnext", - "lib": [ "es2016", "dom" ], + "lib": [ "ES2019", "dom" ], "strict": true, "alwaysStrict": true, - "preserveConstEnums": true + "preserveConstEnums": true, + "esModuleInterop": true, + "moduleResolution": "node" }, "exclude": [ "node_modules" diff --git a/src/Blazor.IntersectionObserver/wwwroot/blazor-intersection-observer.js b/src/Blazor.IntersectionObserver/wwwroot/blazor-intersection-observer.js deleted file mode 100644 index ceb467f..0000000 --- a/src/Blazor.IntersectionObserver/wwwroot/blazor-intersection-observer.js +++ /dev/null @@ -1,158 +0,0 @@ -const OBSERVER_ID_PREFIX = "blazor_plugin_observer__"; -let OBSERVER_ID = 1; -const observerItems = new Map(); -function reset() { - OBSERVER_ID = 1; - observerItems.clear(); -} -function getObserverItems() { - return observerItems; -} -function createObserverItemId() { - return `${OBSERVER_ID_PREFIX}${OBSERVER_ID++}`; -} -function hasSameOptions(a, b) { - return (a.root === b.root && - a.rootMargin === b.rootMargin && - a.threshold === b.threshold); -} -function getItemFromOptions(options) { - let itemFound = null; - for (const [id, instance] of observerItems) { - if (hasSameOptions(options, instance.options)) { - itemFound = { id, instance }; - break; - } - } - return itemFound; -} -function observeInstanceElement(instance, observerId, element) { - const { elements, observer } = instance; - const observerElements = elements.get(observerId); - if (observerElements != null) { - observerElements.add(element); - } - else { - elements.set(observerId, new Set([element])); - } - observer.observe(element); - return instance; -} -function getObserverItem(dotnetRef, options) { - if (options == null) { - options = {}; - } - const observerItem = getItemFromOptions(options); - if (observerItem == null) { - const id = createObserverItemId(); - const observer = new IntersectionObserver(onEntryChange(id), options); - const elements = new Map(); - observerItems.set(id, { dotnetRef, options, observer, elements }); - return { - id, - instance: observerItems.get(id), - }; - } - return observerItem; -} -function create(dotnetRef, id, options) { - const item = getObserverItem(dotnetRef, options); - const { instance } = item; - instance.elements.set(id, new Set([])); - return item; -} -function observe(dotnetRef, id, node, options) { - const { instance } = getObserverItem(dotnetRef, options); - return observeInstanceElement(instance, id, node); -} -function observeElement(id, element) { - const instances = observerItems.values(); - for (const instance of instances) { - const elements = instance.elements.get(id); - if (elements != null && !elements.has(element)) { - instance.observer.observe(element); - elements.add(element); - break; - } - } -} -function cleanupObserver(itemId, instance) { - let instanceRemoved = false; - if (instance.elements.size === 0) { - instance.observer.disconnect(); - observerItems.delete(itemId); - instanceRemoved = true; - } - return instanceRemoved; -} -function unobserve(id, element) { - const instances = observerItems.values(); - let removed = false; - for (const instance of instances) { - const elements = instance.elements.get(id); - if (elements != null && elements.has(element)) { - instance.observer.unobserve(element); - elements.delete(element); - removed = true; - break; - } - } - return removed; -} -function disconnect(id) { - const observers = observerItems.entries(); - let disconnected = false; - for (const [instanceId, instance] of observers) { - if (instance.elements.get(id)) { - instance.elements.delete(id); - cleanupObserver(instanceId, instance); - disconnected = true; - break; - } - } - return disconnected; -} -function toEntryObject(entry) { - function toRectReadOnlyObject(obj) { - return { - X: obj.x, - Y: obj.y, - Width: obj.width, - Height: obj.height, - Top: obj.top, - Left: obj.left, - Bottom: obj.bottom, - Right: obj.right, - }; - } - return { - IsIntersecting: entry.isIntersecting, - IntersectionRatio: entry.intersectionRatio, - Time: entry.time, - BoundingClientRect: toRectReadOnlyObject(entry.boundingClientRect), - IntersectionRect: toRectReadOnlyObject(entry.intersectionRect), - RootBounds: toRectReadOnlyObject(entry.rootBounds), - }; -} -function onEntryChange(observerInstanceId) { - return (entries) => { - if (!observerItems.has(observerInstanceId)) { - return; - } - const { dotnetRef, elements } = observerItems.get(observerInstanceId); - const observerEntries = entries.reduce((batched, entry) => { - for (const [id, item] of elements) { - if (item.has(entry.target)) { - batched[id] = (batched[id] || []).concat(entry); - } - } - return batched; - }, {}); - Object.keys(observerEntries).forEach((observerId) => { - const batch = observerEntries[observerId]; - dotnetRef.invokeMethodAsync("OnCallback", observerId, batch.map(toEntryObject)); - }); - }; -} - -export { OBSERVER_ID_PREFIX, create, disconnect, getObserverItems, observe, observeElement, reset, unobserve }; diff --git a/src/Blazor.IntersectionObserver/wwwroot/blazor-intersection-observer.min.js b/src/Blazor.IntersectionObserver/wwwroot/blazor-intersection-observer.min.js new file mode 100644 index 0000000..0283cf6 --- /dev/null +++ b/src/Blazor.IntersectionObserver/wwwroot/blazor-intersection-observer.min.js @@ -0,0 +1 @@ +const e="blazor_plugin_observer__";let t=1;const n=new Map;function o(){t=0,n.clear()}function r(){return n}function s(e,t,o){const r=function(e){return t=>{if(!n.has(e))return;const{dotnetRef:o}=n.get(e),r=t.map((e=>function(e){function t(e){return{X:e.x,Y:e.y,Width:e.width,Height:e.height,Top:e.top,Left:e.left,Bottom:e.bottom,Right:e.right}}return{IsIntersecting:e.isIntersecting,IntersectionRatio:e.intersectionRatio,Time:e.time,BoundingClientRect:t(e.boundingClientRect),IntersectionRect:t(e.intersectionRect),RootBounds:t(e.rootBounds)}}(e)));o.invokeMethodAsync("OnCallback",e,r)}}(t),s=new IntersectionObserver(r,o);return n.set(t,{dotnetRef:e,observer:s,elements:[]}),n.get(t)}function i(e,o){const r=n.get(e);if(null==r)throw new Error(`Failed to observe element for key: ${e} as the observer does not exist`);if(r.elements.some((e=>e.element==o)))return console.warn(`BlazorIntersectionObserver: The element is already being observed by observer for key ${e}`),"";const s="blazor_plugin_observer__"+t++;return r.observer.observe(o),r.elements.push({elementId:s,element:o}),s}function l(e,t,n){return s(e,t,n)}function c(e,t,n,o){return console.log({dotnetRef:e,callbackId:t,node:n}),s(e,t,o),i(t,n)}function u(e,t){var o;const r=n.get(e);if(null==r)throw new Error(`Failed to unobserve element for key: ${e} as the observer does not exist`);const s=null===(o=r.elements.find((e=>e.element==t)))||void 0===o?void 0:o.elementId;return null==s&&console.warn(`BlazorResizeObserver: The record does not exist for observer: ${e}`),r.observer.unobserve(t),r.elements=r.elements.filter((e=>e.element!=t)),s}function d(e){const t=n.get(e);if(null==t)throw new Error(`Failed to disconnect for key: ${e} as the observer does not exist`);return t.observer.disconnect(),t.elements=[],!0}function f(e){if(d(e))return n.delete(e)}export{e as OBSERVER_ID_PREFIX,l as create,d as disconnect,r as getObserverItems,c as observe,i as observeElement,f as remove,o as reset,u as unobserve}; From 087ab75ece3d7e88ad042bb6074cf21aaec0e807 Mon Sep 17 00:00:00 2001 From: Louie Colgan Date: Wed, 19 May 2021 09:26:48 +0100 Subject: [PATCH 2/7] fix(comment): remove comment --- src/Blazor.IntersectionObserver/src/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Blazor.IntersectionObserver/src/index.ts b/src/Blazor.IntersectionObserver/src/index.ts index 225bd69..0b0b091 100644 --- a/src/Blazor.IntersectionObserver/src/index.ts +++ b/src/Blazor.IntersectionObserver/src/index.ts @@ -121,7 +121,6 @@ export function observe( node: Element, options?: globalThis.IntersectionObserverInit ): string { - console.log({ dotnetRef, callbackId, node }) createObserverItem(dotnetRef, callbackId, options); return observeElement(callbackId, node); } From a71b63d74581fccc290d2ca52224327122282b61 Mon Sep 17 00:00:00 2001 From: Louie Colgan Date: Wed, 19 May 2021 09:30:16 +0100 Subject: [PATCH 3/7] fix(comments): updated correct comments --- src/Blazor.IntersectionObserver/src/index.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Blazor.IntersectionObserver/src/index.ts b/src/Blazor.IntersectionObserver/src/index.ts index 0b0b091..d763b2c 100644 --- a/src/Blazor.IntersectionObserver/src/index.ts +++ b/src/Blazor.IntersectionObserver/src/index.ts @@ -50,7 +50,7 @@ function getObserverElementId() { * @param {DotNetObjectRef} dotnetRef - The current dotnet blazor reference * @param {string} callbackId - The callback id for the blazor observer service * @param {globalThis.IntersectionObserverInit} options - The intersection options - * @returns {IntersectionObserverItem} - The resize observer item + * @returns {IntersectionObserverItem} - The intersection observer item */ function createObserverItem( dotnetRef: DotNetObjectRef, @@ -97,7 +97,7 @@ export function observeElement(callbackId: string, element: Element): string { * @param {IDotNetObjectRef} dotnetRef - The dotnet interop reference * @param {string} callbackId - The callback id for the blazor observer service * @param {globalThis.IntersectionObserverInit} options - The intersection observer options - * @returns {ResizeObserverItem} - The observer item + * @returns {IntersectionObserverItem} - The observer item */ export function create( dotnetRef: DotNetObjectRef, @@ -141,7 +141,7 @@ export function unobserve(callbackId: string, element: Element): string { const unobserveElementId = item.elements.find((record) => record.element == element)?.elementId; if (unobserveElementId == null) { - console.warn(`BlazorResizeObserver: The record does not exist for observer: ${callbackId}`); + console.warn(`BlazorIntersectionObserver: The record does not exist for observer: ${callbackId}`); } item.observer.unobserve(element); @@ -213,9 +213,9 @@ function toEntryObject(entry: IntersectionObserverEntry) { /** * Returns a function that will be triggered when an - * element has an resize update. + * element has an intersection update. * @param {string} callbackId - The observer item id - * @returns {OnIntersectionUpdateFn} - The function triggered by an resize update + * @returns {OnIntersectionUpdateFn} - The function triggered by an intersection update */ function onEntryChange(callbackId: string): OnIntersectionUpdateFn { return (entries: readonly IntersectionObserverEntry[]) => { From 1fb9a93e4de884ca02eb9d27781841ccc9b00e63 Mon Sep 17 00:00:00 2001 From: Louie Colgan Date: Wed, 19 May 2021 09:34:04 +0100 Subject: [PATCH 4/7] fix(examples): no need for null checking --- README.md | 4 ++-- .../Blazor.IntersectionObserver.Client/Pages/LazyImages.razor | 2 +- .../Blazor.IntersectionObserver.Server/Pages/LazyImages.razor | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9af3dae..5bac999 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ object which contains the observer entry! Easy!
- Hey... look I'm @((context?.IsIntersecting ?? false) ? "in view": "out of view") + Hey... I'm @(context.IsIntersecting ? "in view": "out of view")
``` @@ -247,7 +247,7 @@ You need to make sure to provide the reference of the element you want to observ
- Hey... look I'm @((context?.IsIntersecting ?? false) ? "intersecting!": "not intersecting!") + Hey... look I'm @(context.IsIntersecting ? "intersecting!": "not intersecting!")
diff --git a/samples/Blazor.IntersectionObserver.Client/Pages/LazyImages.razor b/samples/Blazor.IntersectionObserver.Client/Pages/LazyImages.razor index 00537d2..e49659e 100644 --- a/samples/Blazor.IntersectionObserver.Client/Pages/LazyImages.razor +++ b/samples/Blazor.IntersectionObserver.Client/Pages/LazyImages.razor @@ -22,7 +22,7 @@
- @{var isIntersecting = (context?.IsIntersecting ?? false);} + @{var isIntersecting = context.IsIntersecting;}
Loading...
diff --git a/samples/Blazor.IntersectionObserver.Server/Pages/LazyImages.razor b/samples/Blazor.IntersectionObserver.Server/Pages/LazyImages.razor index 2225b5d..034c803 100644 --- a/samples/Blazor.IntersectionObserver.Server/Pages/LazyImages.razor +++ b/samples/Blazor.IntersectionObserver.Server/Pages/LazyImages.razor @@ -22,7 +22,7 @@
- @{var isIntersecting = (context?.IsIntersecting ?? false);} + @{var isIntersecting = context.IsIntersecting;}
Loading...
From d64b4ec5593f13a7d28bd53e3fb45420ed8dd02b Mon Sep 17 00:00:00 2001 From: Louie Colgan Date: Wed, 19 May 2021 09:42:47 +0100 Subject: [PATCH 5/7] chore(proj): bump version + updated release notes --- .../Blazor.IntersectionObserver.csproj | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Blazor.IntersectionObserver/Blazor.IntersectionObserver.csproj b/src/Blazor.IntersectionObserver/Blazor.IntersectionObserver.csproj index d4eb849..75b22ab 100644 --- a/src/Blazor.IntersectionObserver/Blazor.IntersectionObserver.csproj +++ b/src/Blazor.IntersectionObserver/Blazor.IntersectionObserver.csproj @@ -16,11 +16,17 @@ Blazor Intersection Observer Intersection Observer API for Blazor applications true - 1.1.0 + 2.0.0 BlazorIntersectionObserver - 06/12/2020 + +19/05/2021 +- *BREAKING CHANGE* The IntersectionObserve component now requires a reference to the node it's observing. +- The imported observer script is now minified. + +06/12/2020 - Updated project to use dotnet 5 LICENCE.txt + Copyright © 2021 - Louie Colgan From a49c2a0ec8640d751048685a9944471ca688f001 Mon Sep 17 00:00:00 2001 From: Louie Colgan Date: Wed, 19 May 2021 09:55:11 +0100 Subject: [PATCH 6/7] chore(pipeline): update pipeline for semantic releases --- azure-pipelines.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index a9f2866..0579917 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -8,6 +8,10 @@ pool: variables: buildConfiguration: 'Release' + variables: + Major: '2' + Minor: '0' + Patch: '0' steps: - task: UseDotNet@2 @@ -53,8 +57,8 @@ steps: packagesToPack: 'src/Blazor.IntersectionObserver/*.csproj' configuration: '$(buildConfiguration)' versioningScheme: byPrereleaseNumber - majorVersion: '0' - minorVersion: '1' - patchVersion: '0' + majorVersion: '$(Major)' + minorVersion: '$(Minor)' + patchVersion: '$(Patch)' From 550fe8f88720b77ed9f9855311571ed57872df97 Mon Sep 17 00:00:00 2001 From: Louie Colgan Date: Wed, 19 May 2021 10:05:29 +0100 Subject: [PATCH 7/7] chore(licence): update year --- LICENCE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENCE.txt b/LICENCE.txt index adabfc0..9d3ad7e 100644 --- a/LICENCE.txt +++ b/LICENCE.txt @@ -1,6 +1,6 @@ (The MIT License) -Copyright (c) 2019 Louie Colgan +Copyright (c) 2021 Louie Colgan Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the