Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@ internal static class BitAppShellJsRuntimeExtensions
{
internal static ValueTask BitAppShellInitScroll(this IJSRuntime jsRuntime, ElementReference container, string url)
{
return jsRuntime.InvokeVoid("BitBlazorUI.AppShell.initScroll", container, url);
return jsRuntime.FastInvokeVoid("BitBlazorUI.AppShell.initScroll", container, url);
}

internal static ValueTask BitAppShellLocationChangedScroll(this IJSRuntime jsRuntime, string url)
{
return jsRuntime.InvokeVoid("BitBlazorUI.AppShell.locationChangedScroll", url);
return jsRuntime.FastInvokeVoid("BitBlazorUI.AppShell.locationChangedScroll", url);
}

internal static ValueTask BitAppShellAfterRenderScroll(this IJSRuntime jsRuntime, string url)
{
return jsRuntime.InvokeVoid("BitBlazorUI.AppShell.afterRenderScroll", url);
return jsRuntime.FastInvokeVoid("BitBlazorUI.AppShell.afterRenderScroll", url);
}

internal static ValueTask BitAppShellDisposeScroll(this IJSRuntime jsRuntime)
{
return jsRuntime.InvokeVoid("BitBlazorUI.AppShell.disposeScroll");
return jsRuntime.FastInvokeVoid("BitBlazorUI.AppShell.disposeScroll");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ namespace BitBlazorUI {

public static updateChart(config: BitChartConfiguration): boolean {
if (!BitChart._bitCharts.has(config.canvasId))
throw `Could not find a chart with the given id. ${config.canvasId}`;
throw `Could not find a chart with the given id: ${config.canvasId}`;

let myChart = BitChart._bitCharts.get(config.canvasId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ internal static class BitChartJsInterop

public static ValueTask BitChartJsRemoveChart(this IJSRuntime jsRuntime, string? canvasId)
{
return jsRuntime.InvokeVoid("BitBlazorUI.BitChart.removeChart", canvasId);
return jsRuntime.FastInvokeVoid("BitBlazorUI.BitChart.removeChart", canvasId);
}

/// <summary>
Expand All @@ -35,7 +35,7 @@ public static ValueTask<bool> BitChartJsSetupChart(this IJSRuntime jsRuntime, Bi
{
var dynParam = StripNulls(chartConfig);
Dictionary<string, object> param = ConvertExpandoObjectToDictionary(dynParam!);
return jsRuntime.Invoke<bool>("BitBlazorUI.BitChart.setupChart", param);
return jsRuntime.FastInvoke<bool>("BitBlazorUI.BitChart.setupChart", param);
}

/// <summary>
Expand All @@ -48,7 +48,7 @@ public static ValueTask<bool> BitChartJsUpdateChart(this IJSRuntime jsRuntime, B
{
var dynParam = StripNulls(chartConfig);
var param = ConvertExpandoObjectToDictionary(dynParam!);
return jsRuntime.Invoke<bool>("BitBlazorUI.BitChart.updateChart", param);
return jsRuntime.FastInvoke<bool>("BitBlazorUI.BitChart.updateChart", param);
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ namespace BitBlazorUI {
colOptions.style.transform = `translateX(${applyOffset}px)`;
}

colOptions.scrollIntoViewIfNeeded();
colOptions.scrollIntoViewIfNeeded?.();

const autoFocusElem = colOptions.querySelector('[autofocus]');
if (autoFocusElem) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@

internal static class BitDataGridJsRuntimeExtensions
{
public static async ValueTask<IJSObjectReference> BitDataGridInit(this IJSRuntime jsRuntime, ElementReference tableElement)
// FastInvoke can return null when the in-process (WASM) path swallows a JSON error (it returns
// default), so the contract is nullable. Callers must null-check before using the reference.
public static async ValueTask<IJSObjectReference?> BitDataGridInit(this IJSRuntime jsRuntime, ElementReference tableElement)
{
return await jsRuntime.Invoke<IJSObjectReference>("BitBlazorUI.DataGrid.init", tableElement);
return await jsRuntime.FastInvoke<IJSObjectReference>("BitBlazorUI.DataGrid.init", tableElement);
}

// This is a fire-and-forget call from OnAfterRenderAsync that runs DOM-heavy positioning logic
// (getBoundingClientRect, scrollIntoViewIfNeeded, focus). It deliberately uses the regular async
// invocation rather than FastInvokeVoid: on WebAssembly FastInvokeVoid runs synchronously and only
// swallows JsonException, so a JS-side failure (e.g. scrollIntoViewIfNeeded being unsupported) would
// throw synchronously and escape the discarded task into the render loop. The async path keeps any
// such failure contained within the returned task instead.
Comment thread
msynk marked this conversation as resolved.
Outdated
public static async ValueTask BitDataGridCheckColumnOptionsPosition(this IJSRuntime jsRuntime, ElementReference tableElement)
{
await jsRuntime.InvokeVoid("BitBlazorUI.DataGrid.checkColumnOptionsPosition", tableElement);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@ public static ValueTask BitInfiniteScrollingSetup<T>(this IJSRuntime jsRuntime,
decimal? threshold,
DotNetObjectReference<BitInfiniteScrolling<T>> dotnetObj)
{
return jsRuntime.InvokeVoid("BitBlazorUI.InfiniteScrolling.setup", id, scrollerSelector, rootElement, lastElement, threshold, dotnetObj);
return jsRuntime.FastInvokeVoid("BitBlazorUI.InfiniteScrolling.setup", id, scrollerSelector, rootElement, lastElement, threshold, dotnetObj);
}

public static ValueTask BitInfiniteScrollingReobserve(this IJSRuntime jsRuntime,
string id,
ElementReference lastElement)
{
return jsRuntime.InvokeVoid("BitBlazorUI.InfiniteScrolling.reobserve", id, lastElement);
return jsRuntime.FastInvokeVoid("BitBlazorUI.InfiniteScrolling.reobserve", id, lastElement);
}

public static ValueTask BitInfiniteScrollingDispose(this IJSRuntime jsRuntime, string id)
{
return jsRuntime.InvokeVoid("BitBlazorUI.InfiniteScrolling.dispose", id);
return jsRuntime.FastInvokeVoid("BitBlazorUI.InfiniteScrolling.dispose", id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,21 @@ public static ValueTask<int> BitPdfReaderSetup(this IJSRuntime jsRuntime, BitPdf

public static ValueTask BitPdfReaderRenderPage(this IJSRuntime jsRuntime, string id, int pageNumber)
{
// The JS renderPage is async (awaits pdf.js page rendering). FastInvoke would use the
// synchronous in-process path in WASM, discarding the returned Promise (fire-and-forget),
// so callers would proceed/raise events before rendering completes and errors would be lost.
return jsRuntime.InvokeVoid("BitBlazorUI.PdfReader.renderPage", id, pageNumber);
}

public static ValueTask BitPdfReaderRefreshPage(this IJSRuntime jsRuntime, BitPdfReaderConfig config, int pageNumber)
{
// The JS refreshPage is async (awaits renderPage). See BitPdfReaderRenderPage for why
// the asynchronous invocation must be used instead of the synchronous fast-invoke.
return jsRuntime.InvokeVoid("BitBlazorUI.PdfReader.refreshPage", config, pageNumber);
}

public static ValueTask BitPdfReaderDispose(this IJSRuntime jsRuntime, string id)
{
return jsRuntime.InvokeVoid("BitBlazorUI.PdfReader.dispose", id);
return jsRuntime.FastInvokeVoid("BitBlazorUI.PdfReader.dispose", id);
}
Comment thread
msynk marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ internal static class ExtrasJsRuntimeExtensions
{
internal static ValueTask BitExtrasApplyRootClasses(this IJSRuntime jsRuntime, List<string> cssClasses, Dictionary<string, string> cssVariables)
{
return jsRuntime.InvokeVoid("BitBlazorUI.Extras.applyRootClasses", cssClasses, cssVariables);
return jsRuntime.FastInvokeVoid("BitBlazorUI.Extras.applyRootClasses", cssClasses, cssVariables);
}

internal static ValueTask BitExtrasGoToTop(this IJSRuntime jsRuntime, ElementReference element, BitScrollBehavior? behavior = null)
{
return jsRuntime.InvokeVoid("BitBlazorUI.Extras.goToTop", element, behavior?.ToString().ToLowerInvariant());
return jsRuntime.FastInvokeVoid("BitBlazorUI.Extras.goToTop", element, behavior?.ToString().ToLowerInvariant());
}

internal static ValueTask BitExtrasScrollBy(this IJSRuntime jsRuntime, ElementReference element, decimal x, decimal y)
{
return jsRuntime.InvokeVoid("BitBlazorUI.Extras.scrollBy", element, x, y);
return jsRuntime.FastInvokeVoid("BitBlazorUI.Extras.scrollBy", element, x, y);
}

public static ValueTask BitExtrasInitScripts(this IJSRuntime jsRuntime, IEnumerable<string> scripts, bool isModule = false)
Expand Down
85 changes: 56 additions & 29 deletions src/BlazorUI/Bit.BlazorUI.Extras/Scripts/Extras.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,42 +19,69 @@ namespace BitBlazorUI {
element.scrollBy(x, y);
}

private static _initScriptsPromises: { [key: string]: Promise<unknown> } = {};
public static async initScripts(scripts: string[], isModule: boolean) {
const key = scripts.join('|');
if (Extras._initScriptsPromises[key] !== undefined) {
return Extras._initScriptsPromises[key];
}

const allScripts = Array.from(document.scripts).map(s => s.src);
const notAddedScripts = scripts.filter(s => !allScripts.find(as => as.includes(s)));
// Resolve only when every script has actually executed. Loading is tracked per-url so that
// concurrent callers (e.g. several components, or a re-mount) await the same execution instead
// of a second caller seeing the <script> tag in the DOM and assuming it is already usable.
await Promise.all((scripts ?? []).map(s => Extras.loadScript(s, isModule)));
}

if (notAddedScripts.length == 0) return Promise.resolve();
private static _scriptPromises: { [url: string]: Promise<void> } = {};
private static loadScript(url: string, isModule: boolean): Promise<void> {
// Track each script by url. Any script this method loads resolves only after its 'load'
// event (i.e. after it has executed), so concurrent/duplicate callers await the real
// execution rather than assuming readiness from the presence of the <script> tag.
const existingPromise = Extras._scriptPromises[url];
if (existingPromise !== undefined) return existingPromise;
Comment thread
msynk marked this conversation as resolved.
Outdated

// A tag we didn't add is host-provided. If the document has finished loading, any non-async
// script has already executed, so it is safe to treat as ready. Otherwise the tag may still be
// loading (e.g. a deferred/async CDN script the host inserted), so await its load/error event
// instead of assuming readiness from the mere presence of the <script> tag. Waiting is gated on
// document.readyState so we never block on a 'load' event that has already fired.
// Match by the full absolute URL (origin + path + query + hash, resolved against the document
// base) so that scripts from different origins (e.g. distinct CDNs) or with different query
// strings (e.g. "?v=1" vs "?v=2") are treated as distinct rather than being conflated by a
// shared pathname. Resolving against baseURI also avoids a substring like "lib.js" matching
// "mylib.js".
const normalize = (u: string) => {
try { return new URL(u, document.baseURI).href; }
catch { return u; }
};
const targetUrl = normalize(url);
const existingTag = Array.from(document.scripts).find(s => !!s.src && normalize(s.src) === targetUrl);
if (existingTag) {
const ready = document.readyState === 'complete'
? Promise.resolve()
: new Promise<void>((res) => {
existingTag.addEventListener('load', () => res(), { once: true });
// A failed host script shouldn't hang every awaiting caller; resolve and let the
// missing global surface as the usual "not a function" error at the call site.
existingTag.addEventListener('error', () => res(), { once: true });
// Final backstop: the window load event fires once all initial resources settle.
window.addEventListener('load', () => res(), { once: true });
});
Extras._scriptPromises[url] = ready;
return ready;
}

const promise = new Promise(async (res: any, rej: any) => {
try {
await Promise.all(notAddedScripts.map(addScript));
res();
} catch (e: any) {
rej(e);
const promise = new Promise<void>((res, rej) => {
const script = document.createElement('script');
script.src = url;
if (isModule) {
script.type = 'module';
}
script.addEventListener('load', () => res());
script.addEventListener('error', rej);
document.body.appendChild(script);
});

Extras._initScriptsPromises[key] = promise;
return promise;
Extras._scriptPromises[url] = promise;

async function addScript(url: string) {
return new Promise((res, rej) => {
const script = document.createElement('script');
script.src = url;
if (isModule) {
script.type = 'module';
}
script.onload = res;
script.onerror = rej;
document.body.appendChild(script);
})
}
// Don't cache a rejected load: a later retry should be able to attempt the script again.
promise.catch(() => { delete Extras._scriptPromises[url]; });

return promise;
}

private static _initStylesheetsPromises: { [key: string]: Promise<unknown> } = {};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,8 @@ private async Task UpdateTime(MouseEventArgs e)
if (IsEnabled is false || ReadOnly || InvalidValueBinding()) return;

var rect = await _js.BitUtilsGetBoundingClientRect(_clockRef);
if (rect is null) return;

var radius = rect.Width / 2;
var centerX = radius;
var centerY = radius;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ internal static class BitCircularTimePickerJsRuntimeExtensions
{
internal static ValueTask<string> BitCircularTimePickerSetup(this IJSRuntime js, DotNetObjectReference<BitCircularTimePicker> obj, string pointerUpHandler, string pointerMoveHandler)
{
return js.Invoke<string>("BitBlazorUI.CircularTimePicker.setup", obj, pointerUpHandler, pointerMoveHandler);
return js.FastInvoke<string>("BitBlazorUI.CircularTimePicker.setup", obj, pointerUpHandler, pointerMoveHandler);
}

internal static ValueTask BitCircularTimePickerDispose(this IJSRuntime jSRuntime, string? abortControllerId)
{
return jSRuntime.InvokeVoid("BitBlazorUI.CircularTimePicker.dispose", abortControllerId);
return jSRuntime.FastInvokeVoid("BitBlazorUI.CircularTimePicker.dispose", abortControllerId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ private async Task UpdateColor(MouseEventArgs e)
if (ColorHasBeenSet && ColorChanged.HasDelegate is false) return;

var pickerRect = await _js.BitUtilsGetBoundingClientRect(_saturationPickerRef);
if (pickerRect is null) return;

var left = e.ClientX < pickerRect.Left
? 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ internal static class BitColorPickerJsRuntimeExtensions
{
internal static ValueTask<string> BitColorPickerSetup(this IJSRuntime js, DotNetObjectReference<BitColorPicker> obj, string pointerUpHandler, string pointerMoveHandler)
{
return js.Invoke<string>("BitBlazorUI.ColorPicker.setup", obj, pointerUpHandler, pointerMoveHandler);
return js.FastInvoke<string>("BitBlazorUI.ColorPicker.setup", obj, pointerUpHandler, pointerMoveHandler);
}

internal static ValueTask BitColorPickerDispose(this IJSRuntime jSRuntime, string? abortControllerId)
{
return jSRuntime.InvokeVoid("BitBlazorUI.ColorPicker.dispose", abortControllerId);
return jSRuntime.FastInvokeVoid("BitBlazorUI.ColorPicker.dispose", abortControllerId);
}
}
Loading
Loading