ng-hub-ui-loading 22.0.0 → 22.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +252 -1
- package/fesm2022/ng-hub-ui-loading.mjs +553 -2
- package/fesm2022/ng-hub-ui-loading.mjs.map +1 -1
- package/package.json +10 -2
- package/styles/_index.scss +1 -0
- package/styles/mixins/_loading-bar-theme.scss +88 -0
- package/types/ng-hub-ui-loading.d.ts +406 -2
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, makeEnvironmentProviders, inject, input, booleanAttribute, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, ApplicationRef, PLATFORM_ID, signal, createComponent, Injectable } from '@angular/core';
|
|
2
|
+
import { InjectionToken, makeEnvironmentProviders, inject, input, booleanAttribute, computed, ViewEncapsulation, ChangeDetectionStrategy, Component, ApplicationRef, PLATFORM_ID, signal, createComponent, Injectable, NgZone, DestroyRef, provideAppInitializer } from '@angular/core';
|
|
3
3
|
import { resolveHubAccent } from 'ng-hub-ui-utils';
|
|
4
4
|
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
|
|
5
|
+
import { HttpContextToken, HttpContext } from '@angular/common/http';
|
|
6
|
+
import { finalize } from 'rxjs';
|
|
7
|
+
import { NavigationEnd, NavigationCancel, NavigationError, NavigationSkipped, Router, NavigationStart } from '@angular/router';
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* Neutral defaults applied when an application provides no configuration.
|
|
@@ -302,11 +305,559 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImpor
|
|
|
302
305
|
args: [{ providedIn: 'root' }]
|
|
303
306
|
}] });
|
|
304
307
|
|
|
308
|
+
/**
|
|
309
|
+
* Default trickle curve: large steps early, then progressively smaller ones.
|
|
310
|
+
*
|
|
311
|
+
* The shape is what makes an invented number believable. Moving fast at the start
|
|
312
|
+
* matches the part of a page load that really is fast (the request goes out, the shell
|
|
313
|
+
* responds); slowing near the end matches the part nobody can predict, and leaves room
|
|
314
|
+
* for `complete()` to arrive without the bar having to jump backwards.
|
|
315
|
+
*
|
|
316
|
+
* Exported so a consumer can wrap it rather than rewrite it.
|
|
317
|
+
*
|
|
318
|
+
* @param progress - Current value, 0–100.
|
|
319
|
+
* @returns Amount to add on this tick.
|
|
320
|
+
*/
|
|
321
|
+
function hubLoadingBarTrickle(progress) {
|
|
322
|
+
if (progress < 20) {
|
|
323
|
+
return 10;
|
|
324
|
+
}
|
|
325
|
+
if (progress < 50) {
|
|
326
|
+
return 4;
|
|
327
|
+
}
|
|
328
|
+
if (progress < 80) {
|
|
329
|
+
return 2;
|
|
330
|
+
}
|
|
331
|
+
if (progress < 99) {
|
|
332
|
+
return 0.5;
|
|
333
|
+
}
|
|
334
|
+
return 0;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Neutral defaults applied when an application provides no configuration.
|
|
338
|
+
*
|
|
339
|
+
* These are the values documented as each input's default, so overriding the token
|
|
340
|
+
* silently re-bases the whole application without touching a template.
|
|
341
|
+
*/
|
|
342
|
+
const HUB_LOADING_BAR_DEFAULT_CONFIG = {
|
|
343
|
+
min: 8,
|
|
344
|
+
max: 99,
|
|
345
|
+
trickleSpeed: 250,
|
|
346
|
+
trickle: true,
|
|
347
|
+
trickleFn: hubLoadingBarTrickle,
|
|
348
|
+
delay: 100,
|
|
349
|
+
completeDelay: 300,
|
|
350
|
+
color: null,
|
|
351
|
+
glow: true,
|
|
352
|
+
ariaLabel: 'Loading'
|
|
353
|
+
};
|
|
354
|
+
/**
|
|
355
|
+
* Resolved defaults shared by `<hub-loading-bar>` and `HubLoadingBarService`.
|
|
356
|
+
*
|
|
357
|
+
* Declared with a root factory so the token is always injectable, even when the
|
|
358
|
+
* application never calls {@link provideHubLoadingBar}.
|
|
359
|
+
*/
|
|
360
|
+
const HUB_LOADING_BAR_CONFIG = new InjectionToken('HUB_LOADING_BAR_CONFIG', {
|
|
361
|
+
providedIn: 'root',
|
|
362
|
+
factory: () => HUB_LOADING_BAR_DEFAULT_CONFIG
|
|
363
|
+
});
|
|
364
|
+
/**
|
|
365
|
+
* Registers application-wide loading-bar defaults — the accent, the pacing, the
|
|
366
|
+
* translated label — so individual call sites stay bare.
|
|
367
|
+
*
|
|
368
|
+
* @param config - Values overriding {@link HUB_LOADING_BAR_DEFAULT_CONFIG}; omitted keys keep their default.
|
|
369
|
+
* @returns Environment providers for the application bootstrap.
|
|
370
|
+
*/
|
|
371
|
+
function provideHubLoadingBar(config = {}) {
|
|
372
|
+
return makeEnvironmentProviders([
|
|
373
|
+
{
|
|
374
|
+
provide: HUB_LOADING_BAR_CONFIG,
|
|
375
|
+
useValue: { ...HUB_LOADING_BAR_DEFAULT_CONFIG, ...config }
|
|
376
|
+
}
|
|
377
|
+
]);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Drives the page-level loading bar: the thin strip under the navbar that says
|
|
382
|
+
* "something is on its way" without pretending to know how long it will take.
|
|
383
|
+
*
|
|
384
|
+
* Three decisions shape the whole service, and each exists because the naive version is
|
|
385
|
+
* worse:
|
|
386
|
+
*
|
|
387
|
+
* - **Reference counting, not a boolean.** A route change and the three requests its
|
|
388
|
+
* page fires are four independent callers. With a boolean, the first one to finish
|
|
389
|
+
* would take the bar down while the other three were still working. The bar completes
|
|
390
|
+
* when the count reaches zero, and {@link completeAll} is the escape hatch for a caller
|
|
391
|
+
* that never balanced its `start()`.
|
|
392
|
+
* - **A grace period before anything is painted.** Work that finishes within `delay`
|
|
393
|
+
* never shows a bar at all. A cached route that flashes a progress bar for 40 ms reads
|
|
394
|
+
* as a glitch, not as speed.
|
|
395
|
+
* - **A trickle that never reaches the end.** Nothing here knows the real percentage, so
|
|
396
|
+
* the bar advances in shrinking steps and stops at `max`. Only `complete()` may show
|
|
397
|
+
* 100, because only `complete()` knows it is true.
|
|
398
|
+
*
|
|
399
|
+
* Timers run outside the Angular zone. Inside it, a 250 ms interval would trigger change
|
|
400
|
+
* detection for the whole application on every tick and — far worse — would keep
|
|
401
|
+
* `ApplicationRef.isStable` false forever, which hangs server-side rendering. The state
|
|
402
|
+
* is signals, so change detection is still scheduled correctly when a value actually
|
|
403
|
+
* changes. On the server no timer is created at all: the counter stays truthful and the
|
|
404
|
+
* rendered HTML carries no bar to mismatch on hydration.
|
|
405
|
+
*
|
|
406
|
+
* @example
|
|
407
|
+
* ```typescript
|
|
408
|
+
* private readonly bar = inject(HubLoadingBarService);
|
|
409
|
+
*
|
|
410
|
+
* async import(): Promise<void> {
|
|
411
|
+
* this.bar.start();
|
|
412
|
+
* try {
|
|
413
|
+
* await this.api.import();
|
|
414
|
+
* } finally {
|
|
415
|
+
* this.bar.complete();
|
|
416
|
+
* }
|
|
417
|
+
* }
|
|
418
|
+
* ```
|
|
419
|
+
*/
|
|
420
|
+
class HubLoadingBarService {
|
|
421
|
+
config = inject(HUB_LOADING_BAR_CONFIG);
|
|
422
|
+
zone = inject(NgZone);
|
|
423
|
+
platformId = inject(PLATFORM_ID);
|
|
424
|
+
/** Number of callers currently waiting on something. */
|
|
425
|
+
pending = signal(0, /* @ts-ignore */
|
|
426
|
+
...(ngDevMode ? [{ debugName: "pending" }] : /* istanbul ignore next */ []));
|
|
427
|
+
/** Current fill, 0–100. */
|
|
428
|
+
_progress = signal(0, /* @ts-ignore */
|
|
429
|
+
...(ngDevMode ? [{ debugName: "_progress" }] : /* istanbul ignore next */ []));
|
|
430
|
+
/** Whether the bar is painted right now; false during the grace period. */
|
|
431
|
+
_visible = signal(false, /* @ts-ignore */
|
|
432
|
+
...(ngDevMode ? [{ debugName: "_visible" }] : /* istanbul ignore next */ []));
|
|
433
|
+
/** Handles of the running timers, so each can be cancelled independently. */
|
|
434
|
+
timers = {
|
|
435
|
+
reveal: null,
|
|
436
|
+
trickle: null,
|
|
437
|
+
complete: null
|
|
438
|
+
};
|
|
439
|
+
/** Current fill, 0–100. Bound by `<hub-loading-bar>`; safe to read during SSR. */
|
|
440
|
+
progress = this._progress.asReadonly();
|
|
441
|
+
/**
|
|
442
|
+
* Whether the bar is on screen. Differs from {@link isActive} at both ends of a cycle:
|
|
443
|
+
* false while the grace period runs, and still true during the completion tail.
|
|
444
|
+
*/
|
|
445
|
+
isVisible = this._visible.asReadonly();
|
|
446
|
+
/**
|
|
447
|
+
* Whether any caller is still waiting — the honest "is the page loading?" question,
|
|
448
|
+
* regardless of whether the bar has decided to show itself yet.
|
|
449
|
+
*/
|
|
450
|
+
isActive = computed(() => this.pending() > 0, /* @ts-ignore */
|
|
451
|
+
...(ngDevMode ? [{ debugName: "isActive" }] : /* istanbul ignore next */ []));
|
|
452
|
+
constructor() {
|
|
453
|
+
// A root service outlives most things, but not a TestBed reset or an HMR reload;
|
|
454
|
+
// an orphan interval would keep ticking against a dead injector.
|
|
455
|
+
inject(DestroyRef).onDestroy(() => this.clearAllTimers());
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Registers one caller. The first one starts a cycle; the rest simply join the count.
|
|
459
|
+
*/
|
|
460
|
+
start() {
|
|
461
|
+
const wasIdle = this.pending() === 0;
|
|
462
|
+
this.pending.update((count) => count + 1);
|
|
463
|
+
if (!wasIdle) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (this.timers.complete !== null) {
|
|
467
|
+
// Interrupting the completion tail. Drop the bar before rewinding it: the
|
|
468
|
+
// stylesheet only transitions the fill while the bar is visible, so rewinding
|
|
469
|
+
// it on screen would animate the progress running backwards.
|
|
470
|
+
this.clearTimer('complete');
|
|
471
|
+
this._visible.set(false);
|
|
472
|
+
}
|
|
473
|
+
this._progress.set(this.config.min);
|
|
474
|
+
if (!isPlatformBrowser(this.platformId)) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
// `delay: 0` has to mean "paint now", not "paint on the next macrotask". A router
|
|
478
|
+
// navigation with no async guard settles inside the same task it started in, so a
|
|
479
|
+
// deferred reveal would be cancelled by `complete()` before it ever ran and the bar
|
|
480
|
+
// would be unreachable for anyone who opted out of the grace period.
|
|
481
|
+
if (this.config.delay <= 0) {
|
|
482
|
+
this.reveal();
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
this.schedule('reveal', this.config.delay, () => this.reveal());
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Retires one caller. Once none are left the bar runs to 100% and fades away — or, if
|
|
489
|
+
* the grace period swallowed the whole operation, disappears without ever having been
|
|
490
|
+
* seen.
|
|
491
|
+
*/
|
|
492
|
+
complete() {
|
|
493
|
+
this.pending.update((count) => Math.max(0, count - 1));
|
|
494
|
+
if (this.pending() === 0) {
|
|
495
|
+
this.finish();
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
/** Drops every pending caller and completes the bar immediately. */
|
|
499
|
+
completeAll() {
|
|
500
|
+
this.pending.set(0);
|
|
501
|
+
this.finish();
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Moves the bar to an exact value and reveals it, bypassing the grace period.
|
|
505
|
+
*
|
|
506
|
+
* For work whose real percentage is known — a file upload, a paged import. The value
|
|
507
|
+
* is not capped at `max`, because a caller reporting a true 100 is not guessing;
|
|
508
|
+
* finishing the cycle is still {@link complete}'s job.
|
|
509
|
+
*
|
|
510
|
+
* @param value - Target fill, 0–100. Values outside the range are clamped.
|
|
511
|
+
*/
|
|
512
|
+
set(value) {
|
|
513
|
+
this.reveal();
|
|
514
|
+
this._progress.set(Math.min(100, Math.max(0, value)));
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Advances the bar and reveals it, bypassing the grace period.
|
|
518
|
+
*
|
|
519
|
+
* @param amount - Step to add. Omitted, the configured trickle curve decides, which is
|
|
520
|
+
* what makes an unknown wait keep moving without ever arriving.
|
|
521
|
+
*/
|
|
522
|
+
inc(amount) {
|
|
523
|
+
this.reveal();
|
|
524
|
+
this.advance(amount);
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Cancels everything at once: no completion animation, no pending callers, nothing on
|
|
528
|
+
* screen. For an error handler that wants the bar gone rather than finished.
|
|
529
|
+
*/
|
|
530
|
+
reset() {
|
|
531
|
+
this.clearAllTimers();
|
|
532
|
+
this.pending.set(0);
|
|
533
|
+
this._visible.set(false);
|
|
534
|
+
this._progress.set(0);
|
|
535
|
+
}
|
|
536
|
+
/** Paints the bar and starts the trickle, if a caller is waiting on one. */
|
|
537
|
+
reveal() {
|
|
538
|
+
this.clearTimer('reveal');
|
|
539
|
+
this._visible.set(true);
|
|
540
|
+
this.startTrickling();
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Runs the completion, shared by {@link complete} and {@link completeAll}.
|
|
544
|
+
*
|
|
545
|
+
* The fill is deliberately left at 100 once the bar is hidden. `start()` rewinds it
|
|
546
|
+
* while the bar is invisible, where the stylesheet suppresses the transition, so the
|
|
547
|
+
* next cycle begins from `min` without the previous one being seen to unwind.
|
|
548
|
+
*/
|
|
549
|
+
finish() {
|
|
550
|
+
this.clearTimer('reveal');
|
|
551
|
+
this.clearTimer('trickle');
|
|
552
|
+
if (!this._visible()) {
|
|
553
|
+
this._progress.set(0);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
this._progress.set(100);
|
|
557
|
+
this.schedule('complete', this.config.completeDelay, () => this._visible.set(false));
|
|
558
|
+
}
|
|
559
|
+
/** Starts the trickle interval, unless it is disabled, already running or unneeded. */
|
|
560
|
+
startTrickling() {
|
|
561
|
+
if (!this.config.trickle || this.timers.trickle !== null || this.pending() === 0) {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (!isPlatformBrowser(this.platformId)) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
this.zone.runOutsideAngular(() => {
|
|
568
|
+
this.timers.trickle = setInterval(() => this.advance(), this.config.trickleSpeed);
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
/** Adds one step, capped at `max` so the trickle can never claim to be finished. */
|
|
572
|
+
advance(amount) {
|
|
573
|
+
const current = this._progress();
|
|
574
|
+
const step = amount ?? this.config.trickleFn(current);
|
|
575
|
+
this._progress.set(Math.min(this.config.max, Math.max(0, current + step)));
|
|
576
|
+
}
|
|
577
|
+
/** Schedules a one-shot timer outside the Angular zone, replacing any previous one. */
|
|
578
|
+
schedule(name, delay, action) {
|
|
579
|
+
this.clearTimer(name);
|
|
580
|
+
this.zone.runOutsideAngular(() => {
|
|
581
|
+
this.timers[name] = setTimeout(() => {
|
|
582
|
+
this.timers[name] = null;
|
|
583
|
+
action();
|
|
584
|
+
}, delay);
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
/** Cancels one timer if it is running. Safe to call for a timer that is not. */
|
|
588
|
+
clearTimer(name) {
|
|
589
|
+
const handle = this.timers[name];
|
|
590
|
+
if (handle === null) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
this.timers[name] = null;
|
|
594
|
+
// `setInterval` and `setTimeout` share a handle space in both runtimes, so one
|
|
595
|
+
// clear covers the repeating trickle and the two one-shots alike.
|
|
596
|
+
clearTimeout(handle);
|
|
597
|
+
clearInterval(handle);
|
|
598
|
+
}
|
|
599
|
+
/** Cancels every timer; used by `reset()` and on injector teardown. */
|
|
600
|
+
clearAllTimers() {
|
|
601
|
+
this.clearTimer('reveal');
|
|
602
|
+
this.clearTimer('trickle');
|
|
603
|
+
this.clearTimer('complete');
|
|
604
|
+
}
|
|
605
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: HubLoadingBarService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
606
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: HubLoadingBarService, providedIn: 'root' });
|
|
607
|
+
}
|
|
608
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: HubLoadingBarService, decorators: [{
|
|
609
|
+
type: Injectable,
|
|
610
|
+
args: [{ providedIn: 'root' }]
|
|
611
|
+
}], ctorParameters: () => [] });
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* The thin strip that reports page-level progress — the bar under the navbar.
|
|
615
|
+
*
|
|
616
|
+
* By default the component draws whatever {@link HubLoadingBarService} is doing, which is
|
|
617
|
+
* what makes a single `<hub-loading-bar />` in the shell enough for the whole
|
|
618
|
+
* application: the router integration and the HTTP interceptor drive the service, and
|
|
619
|
+
* this element follows. Bind `progress` to take it over instead, for a bar reporting one
|
|
620
|
+
* known quantity — an upload, an import — independently of everything else.
|
|
621
|
+
*
|
|
622
|
+
* The `progress` input therefore has three meanings, and the difference matters:
|
|
623
|
+
* unbound (`undefined`) follows the service; a number drives the bar directly; `null`
|
|
624
|
+
* hides it. This mirrors how `HubLoadingService` already reads `undefined` as "leave it
|
|
625
|
+
* alone" and `null` as "clear it".
|
|
626
|
+
*
|
|
627
|
+
* Note the deliberate gap in the accessibility contract. While the service is trickling,
|
|
628
|
+
* the number on screen is invented — nothing knows the real percentage of a page load —
|
|
629
|
+
* so `aria-valuenow` is withheld, which is exactly how ARIA spells an indeterminate
|
|
630
|
+
* progressbar. Announcing a made-up "43%" would be worse than announcing nothing. The
|
|
631
|
+
* value is published only when a caller has bound a real one.
|
|
632
|
+
*
|
|
633
|
+
* Styles are unencapsulated, like the rest of the library, so a consumer can retheme the
|
|
634
|
+
* bar from a global stylesheet.
|
|
635
|
+
*
|
|
636
|
+
* @example
|
|
637
|
+
* ```html
|
|
638
|
+
* <!-- Hanging off the bottom edge of a navbar, driven by the service -->
|
|
639
|
+
* <nav class="navbar position-relative">
|
|
640
|
+
* …
|
|
641
|
+
* <hub-loading-bar mode="overlay" placement="bottom" />
|
|
642
|
+
* </nav>
|
|
643
|
+
*
|
|
644
|
+
* <!-- Reporting one known quantity -->
|
|
645
|
+
* <hub-loading-bar [progress]="uploaded()" color="success" />
|
|
646
|
+
* ```
|
|
647
|
+
*/
|
|
648
|
+
class HubLoadingBarComponent {
|
|
649
|
+
/** Application-wide defaults; also the source of every input's default value. */
|
|
650
|
+
config = inject(HUB_LOADING_BAR_CONFIG);
|
|
651
|
+
/** The shared page-level state this bar renders unless `progress` is bound. */
|
|
652
|
+
service = inject(HubLoadingBarService);
|
|
653
|
+
/**
|
|
654
|
+
* Placement of the strip. `overlay` needs a positioned ancestor to attach to;
|
|
655
|
+
* `fixed` pins it to the viewport at `--hub-loading-bar-offset`.
|
|
656
|
+
*/
|
|
657
|
+
mode = input('inline', /* @ts-ignore */
|
|
658
|
+
...(ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []));
|
|
659
|
+
/** Edge the `overlay` and `fixed` modes attach to; `inline` ignores it. */
|
|
660
|
+
placement = input('top', /* @ts-ignore */
|
|
661
|
+
...(ngDevMode ? [{ debugName: "placement" }] : /* istanbul ignore next */ []));
|
|
662
|
+
/**
|
|
663
|
+
* Takes the bar over. Leave it unbound to follow {@link HubLoadingBarService}; bind a
|
|
664
|
+
* number (0–100) to drive it directly, or `null` to hide it.
|
|
665
|
+
*/
|
|
666
|
+
progress = input(undefined, /* @ts-ignore */
|
|
667
|
+
...(ngDevMode ? [{ debugName: "progress" }] : /* istanbul ignore next */ []));
|
|
668
|
+
/**
|
|
669
|
+
* Sweeps a fragment back and forth instead of filling.
|
|
670
|
+
*
|
|
671
|
+
* The honest choice when there is no percentage worth inventing — a long stream, a
|
|
672
|
+
* job with no reported stages.
|
|
673
|
+
*/
|
|
674
|
+
indeterminate = input(false, { ...(ngDevMode ? { debugName: "indeterminate" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
675
|
+
/** Soft glow trailing the leading edge, which is what reads as movement. */
|
|
676
|
+
glow = input(this.config.glow, { ...(ngDevMode ? { debugName: "glow" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
|
|
677
|
+
/**
|
|
678
|
+
* Accent for the fill. Accepts a semantic name (`primary`), a CSS colour literal or a
|
|
679
|
+
* `var(...)` reference — normalised by `resolveHubAccent()` into the single
|
|
680
|
+
* `--hub-loading-bar-accent` slot.
|
|
681
|
+
*/
|
|
682
|
+
color = input(this.config.color, /* @ts-ignore */
|
|
683
|
+
...(ngDevMode ? [{ debugName: "color" }] : /* istanbul ignore next */ []));
|
|
684
|
+
/** Accessible name for the host's `role="progressbar"`. */
|
|
685
|
+
ariaLabel = input(this.config.ariaLabel, /* @ts-ignore */
|
|
686
|
+
...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
|
|
687
|
+
/** Whether a caller has taken the bar over rather than following the service. */
|
|
688
|
+
_manual = computed(() => this.progress() !== undefined, /* @ts-ignore */
|
|
689
|
+
...(ngDevMode ? [{ debugName: "_manual" }] : /* istanbul ignore next */ []));
|
|
690
|
+
/** Current fill, from whichever source is in charge. */
|
|
691
|
+
_value = computed(() => (this._manual() ? (this.progress() ?? 0) : this.service.progress()), /* @ts-ignore */
|
|
692
|
+
...(ngDevMode ? [{ debugName: "_value" }] : /* istanbul ignore next */ []));
|
|
693
|
+
/** Whether the strip is painted at all. */
|
|
694
|
+
_visible = computed(() => (this._manual() ? this.progress() !== null : this.service.isVisible()), /* @ts-ignore */
|
|
695
|
+
...(ngDevMode ? [{ debugName: "_visible" }] : /* istanbul ignore next */ []));
|
|
696
|
+
/** Fill as a CSS length, consumed by the stylesheet's single runtime slot. */
|
|
697
|
+
_fill = computed(() => `${this._value()}%`, /* @ts-ignore */
|
|
698
|
+
...(ngDevMode ? [{ debugName: "_fill" }] : /* istanbul ignore next */ []));
|
|
699
|
+
/**
|
|
700
|
+
* The value published to assistive technology: only ever a real one.
|
|
701
|
+
*
|
|
702
|
+
* `null` removes the attribute, which is how ARIA marks a progressbar indeterminate —
|
|
703
|
+
* the correct answer both while the service trickles an invented number and while the
|
|
704
|
+
* `indeterminate` sweep is running. A bar that is not painted publishes nothing either:
|
|
705
|
+
* the host is `aria-hidden` by then, so a stale value would only ever be misleading.
|
|
706
|
+
*/
|
|
707
|
+
_announcedValue = computed(() => this._visible() && this._manual() && !this.indeterminate() ? this._value() : null, /* @ts-ignore */
|
|
708
|
+
...(ngDevMode ? [{ debugName: "_announcedValue" }] : /* istanbul ignore next */ []));
|
|
709
|
+
/** Mode and placement modifiers; kept as one binding so neither can drop the other. */
|
|
710
|
+
_modifierClasses = computed(() => {
|
|
711
|
+
const classes = [`hub-loading-bar--${this.mode()}`];
|
|
712
|
+
if (this.mode() !== 'inline') {
|
|
713
|
+
classes.push(`hub-loading-bar--${this.placement()}`);
|
|
714
|
+
}
|
|
715
|
+
if (this.indeterminate()) {
|
|
716
|
+
classes.push('hub-loading-bar--indeterminate');
|
|
717
|
+
}
|
|
718
|
+
if (this.glow()) {
|
|
719
|
+
classes.push('hub-loading-bar--glow');
|
|
720
|
+
}
|
|
721
|
+
return classes.join(' ');
|
|
722
|
+
}, /* @ts-ignore */
|
|
723
|
+
...(ngDevMode ? [{ debugName: "_modifierClasses" }] : /* istanbul ignore next */ []));
|
|
724
|
+
/**
|
|
725
|
+
* Single accent slot consumed by the stylesheet. `null` leaves the binding off
|
|
726
|
+
* entirely, so the token's own cascade default stays in effect.
|
|
727
|
+
*/
|
|
728
|
+
_accent = computed(() => resolveHubAccent(this.color()), /* @ts-ignore */
|
|
729
|
+
...(ngDevMode ? [{ debugName: "_accent" }] : /* istanbul ignore next */ []));
|
|
730
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: HubLoadingBarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
731
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "22.0.8", type: HubLoadingBarComponent, isStandalone: true, selector: "hub-loading-bar", inputs: { mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "placement", isSignal: true, isRequired: false, transformFunction: null }, progress: { classPropertyName: "progress", publicName: "progress", isSignal: true, isRequired: false, transformFunction: null }, indeterminate: { classPropertyName: "indeterminate", publicName: "indeterminate", isSignal: true, isRequired: false, transformFunction: null }, glow: { classPropertyName: "glow", publicName: "glow", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, host: { attributes: { "role": "progressbar", "aria-valuemin": "0", "aria-valuemax": "100" }, properties: { "class": "_modifierClasses()", "class.hub-loading-bar--visible": "_visible()", "attr.aria-label": "ariaLabel()", "attr.aria-valuenow": "_announcedValue()", "attr.aria-hidden": "_visible() ? null : \"true\"", "style.--hub-loading-bar-progress": "_fill()", "style.--hub-loading-bar-accent": "_accent()" }, classAttribute: "hub-loading-bar" }, ngImport: i0, template: "<span class=\"hub-loading-bar__indicator\"></span>\n", styles: [":where(.hub-loading-bar){--hub-loading-bar-accent: var(--hub-sys-color-primary, #0d6efd);--hub-loading-bar-height: 3px;--hub-loading-bar-track-bg: transparent;--hub-loading-bar-radius: 0;--hub-loading-bar-speed: .2s;--hub-loading-bar-fade: .3s;--hub-loading-bar-easing: linear;--hub-loading-bar-glow-color: var(--hub-loading-bar-accent);--hub-loading-bar-glow-blur: 10px;--hub-loading-bar-glow-spread: 1px;--hub-loading-bar-indeterminate-speed: 1.6s;--hub-loading-bar-offset: 0px;--hub-loading-bar-z-index: var(--hub-sys-zindex-sticky, 1020);--hub-loading-bar-progress: 0%;--hub-loading-bar-sweep-direction: 1}[dir=rtl] .hub-loading-bar{--hub-loading-bar-sweep-direction: -1}.hub-loading-bar{display:block;position:relative;overflow:hidden;inline-size:100%;block-size:var(--hub-loading-bar-height);background:var(--hub-loading-bar-track-bg);border-radius:var(--hub-loading-bar-radius);opacity:0;pointer-events:none;transition:opacity var(--hub-loading-bar-fade) ease}.hub-loading-bar--visible{opacity:1}.hub-loading-bar--overlay,.hub-loading-bar--fixed{inset-inline:0;z-index:var(--hub-loading-bar-z-index)}.hub-loading-bar--overlay{position:absolute}.hub-loading-bar--fixed{position:fixed}.hub-loading-bar--top{inset-block-start:var(--hub-loading-bar-offset)}.hub-loading-bar--bottom{inset-block-end:var(--hub-loading-bar-offset)}.hub-loading-bar__indicator{display:block;position:relative;inline-size:var(--hub-loading-bar-progress);block-size:100%;background:var(--hub-loading-bar-accent);border-radius:inherit}.hub-loading-bar--visible .hub-loading-bar__indicator{transition:inline-size var(--hub-loading-bar-speed) var(--hub-loading-bar-easing)}.hub-loading-bar--glow .hub-loading-bar__indicator:after{content:\"\";position:absolute;inset-block:0;inset-inline-end:0;inline-size:var(--hub-loading-bar-height);box-shadow:0 0 var(--hub-loading-bar-glow-blur) var(--hub-loading-bar-glow-spread) var(--hub-loading-bar-glow-color),0 0 calc(var(--hub-loading-bar-glow-blur) / 2) var(--hub-loading-bar-glow-color)}.hub-loading-bar--indeterminate .hub-loading-bar__indicator,.hub-loading-bar--visible.hub-loading-bar--indeterminate .hub-loading-bar__indicator{inline-size:33%;transition:none;animation:hub-loading-bar-sweep var(--hub-loading-bar-indeterminate-speed) ease-in-out infinite}@media(prefers-reduced-motion:reduce){.hub-loading-bar{--hub-loading-bar-indeterminate-speed: 3.2s}}@keyframes hub-loading-bar-sweep{0%{transform:translate(calc(-100% * var(--hub-loading-bar-sweep-direction)))}to{transform:translate(calc(300% * var(--hub-loading-bar-sweep-direction)))}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
|
|
732
|
+
}
|
|
733
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: HubLoadingBarComponent, decorators: [{
|
|
734
|
+
type: Component,
|
|
735
|
+
args: [{ selector: 'hub-loading-bar', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: {
|
|
736
|
+
class: 'hub-loading-bar',
|
|
737
|
+
role: 'progressbar',
|
|
738
|
+
'aria-valuemin': '0',
|
|
739
|
+
'aria-valuemax': '100',
|
|
740
|
+
'[class]': '_modifierClasses()',
|
|
741
|
+
'[class.hub-loading-bar--visible]': '_visible()',
|
|
742
|
+
'[attr.aria-label]': 'ariaLabel()',
|
|
743
|
+
'[attr.aria-valuenow]': '_announcedValue()',
|
|
744
|
+
'[attr.aria-hidden]': '_visible() ? null : "true"',
|
|
745
|
+
'[style.--hub-loading-bar-progress]': '_fill()',
|
|
746
|
+
'[style.--hub-loading-bar-accent]': '_accent()'
|
|
747
|
+
}, template: "<span class=\"hub-loading-bar__indicator\"></span>\n", styles: [":where(.hub-loading-bar){--hub-loading-bar-accent: var(--hub-sys-color-primary, #0d6efd);--hub-loading-bar-height: 3px;--hub-loading-bar-track-bg: transparent;--hub-loading-bar-radius: 0;--hub-loading-bar-speed: .2s;--hub-loading-bar-fade: .3s;--hub-loading-bar-easing: linear;--hub-loading-bar-glow-color: var(--hub-loading-bar-accent);--hub-loading-bar-glow-blur: 10px;--hub-loading-bar-glow-spread: 1px;--hub-loading-bar-indeterminate-speed: 1.6s;--hub-loading-bar-offset: 0px;--hub-loading-bar-z-index: var(--hub-sys-zindex-sticky, 1020);--hub-loading-bar-progress: 0%;--hub-loading-bar-sweep-direction: 1}[dir=rtl] .hub-loading-bar{--hub-loading-bar-sweep-direction: -1}.hub-loading-bar{display:block;position:relative;overflow:hidden;inline-size:100%;block-size:var(--hub-loading-bar-height);background:var(--hub-loading-bar-track-bg);border-radius:var(--hub-loading-bar-radius);opacity:0;pointer-events:none;transition:opacity var(--hub-loading-bar-fade) ease}.hub-loading-bar--visible{opacity:1}.hub-loading-bar--overlay,.hub-loading-bar--fixed{inset-inline:0;z-index:var(--hub-loading-bar-z-index)}.hub-loading-bar--overlay{position:absolute}.hub-loading-bar--fixed{position:fixed}.hub-loading-bar--top{inset-block-start:var(--hub-loading-bar-offset)}.hub-loading-bar--bottom{inset-block-end:var(--hub-loading-bar-offset)}.hub-loading-bar__indicator{display:block;position:relative;inline-size:var(--hub-loading-bar-progress);block-size:100%;background:var(--hub-loading-bar-accent);border-radius:inherit}.hub-loading-bar--visible .hub-loading-bar__indicator{transition:inline-size var(--hub-loading-bar-speed) var(--hub-loading-bar-easing)}.hub-loading-bar--glow .hub-loading-bar__indicator:after{content:\"\";position:absolute;inset-block:0;inset-inline-end:0;inline-size:var(--hub-loading-bar-height);box-shadow:0 0 var(--hub-loading-bar-glow-blur) var(--hub-loading-bar-glow-spread) var(--hub-loading-bar-glow-color),0 0 calc(var(--hub-loading-bar-glow-blur) / 2) var(--hub-loading-bar-glow-color)}.hub-loading-bar--indeterminate .hub-loading-bar__indicator,.hub-loading-bar--visible.hub-loading-bar--indeterminate .hub-loading-bar__indicator{inline-size:33%;transition:none;animation:hub-loading-bar-sweep var(--hub-loading-bar-indeterminate-speed) ease-in-out infinite}@media(prefers-reduced-motion:reduce){.hub-loading-bar{--hub-loading-bar-indeterminate-speed: 3.2s}}@keyframes hub-loading-bar-sweep{0%{transform:translate(calc(-100% * var(--hub-loading-bar-sweep-direction)))}to{transform:translate(calc(300% * var(--hub-loading-bar-sweep-direction)))}}\n"] }]
|
|
748
|
+
}], propDecorators: { mode: [{ type: i0.Input, args: [{ isSignal: true, alias: "mode", required: false }] }], placement: [{ type: i0.Input, args: [{ isSignal: true, alias: "placement", required: false }] }], progress: [{ type: i0.Input, args: [{ isSignal: true, alias: "progress", required: false }] }], indeterminate: [{ type: i0.Input, args: [{ isSignal: true, alias: "indeterminate", required: false }] }], glow: [{ type: i0.Input, args: [{ isSignal: true, alias: "glow", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }] } });
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Marks a request as invisible to the loading bar.
|
|
752
|
+
*
|
|
753
|
+
* The escape hatch is not a nicety. A poll on a timer, a heartbeat, an autosave — any
|
|
754
|
+
* request the reader did not ask for — would otherwise hold the bar open forever, and a
|
|
755
|
+
* progress bar that never finishes is worse than none.
|
|
756
|
+
*/
|
|
757
|
+
const HUB_LOADING_BAR_SKIP = new HttpContextToken(() => false);
|
|
758
|
+
/**
|
|
759
|
+
* Builds the `HttpContext` that hides one request from the loading bar.
|
|
760
|
+
*
|
|
761
|
+
* @param context - Existing context to extend; a fresh one by default.
|
|
762
|
+
* @returns The context, with the skip flag set.
|
|
763
|
+
*
|
|
764
|
+
* @example
|
|
765
|
+
* ```typescript
|
|
766
|
+
* this.http.get('/api/notifications', { context: withoutHubLoadingBar() });
|
|
767
|
+
* ```
|
|
768
|
+
*/
|
|
769
|
+
function withoutHubLoadingBar(context = new HttpContext()) {
|
|
770
|
+
return context.set(HUB_LOADING_BAR_SKIP, true);
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Holds the loading bar open for the lifetime of every HTTP request.
|
|
774
|
+
*
|
|
775
|
+
* The bar's reference counter is what makes this safe to combine with the router
|
|
776
|
+
* integration and with hand-written `start()` calls: six parallel requests are six
|
|
777
|
+
* callers, and the bar completes when the last of them does, not the first.
|
|
778
|
+
*
|
|
779
|
+
* `finalize` is the balancing point rather than a `tap` on success, because it also fires
|
|
780
|
+
* when the request errors and when the caller unsubscribes — a cancelled typeahead is the
|
|
781
|
+
* commonest way a naive interceptor strands the bar at 90%.
|
|
782
|
+
*
|
|
783
|
+
* Opt a request out with {@link withoutHubLoadingBar}.
|
|
784
|
+
*
|
|
785
|
+
* @example
|
|
786
|
+
* ```typescript
|
|
787
|
+
* provideHttpClient(withInterceptors([hubLoadingBarInterceptor]))
|
|
788
|
+
* ```
|
|
789
|
+
*/
|
|
790
|
+
const hubLoadingBarInterceptor = (req, next) => {
|
|
791
|
+
if (req.context.get(HUB_LOADING_BAR_SKIP)) {
|
|
792
|
+
return next(req);
|
|
793
|
+
}
|
|
794
|
+
const bar = inject(HubLoadingBarService);
|
|
795
|
+
bar.start();
|
|
796
|
+
return next(req).pipe(finalize(() => bar.complete()));
|
|
797
|
+
};
|
|
798
|
+
|
|
799
|
+
/** Every way a navigation can stop, successful or not. All four must release the bar. */
|
|
800
|
+
function isNavigationSettled(event) {
|
|
801
|
+
return (event instanceof NavigationEnd ||
|
|
802
|
+
event instanceof NavigationCancel ||
|
|
803
|
+
event instanceof NavigationError ||
|
|
804
|
+
event instanceof NavigationSkipped);
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Drives the loading bar from router navigation, which is the "page is loading" the bar
|
|
808
|
+
* is named after: it starts when a navigation begins and completes when it settles —
|
|
809
|
+
* including when it is cancelled by a guard or fails, because a bar left running after a
|
|
810
|
+
* rejected navigation is a bar that never goes away.
|
|
811
|
+
*
|
|
812
|
+
* Navigations are tracked with a flag rather than by pairing events one-to-one. The
|
|
813
|
+
* router's event sequence varies with configuration (a blocking initial navigation, a
|
|
814
|
+
* redirect, a skipped same-URL navigation), and the flag guarantees exactly one
|
|
815
|
+
* `start()` / `complete()` pair per navigation whatever order the events arrive in — a
|
|
816
|
+
* missed `NavigationStart` during bootstrap can no longer leave an unmatched `complete()`
|
|
817
|
+
* decrementing somebody else's count.
|
|
818
|
+
*
|
|
819
|
+
* Combine with {@link hubLoadingBarInterceptor} when routes fetch their own data: the
|
|
820
|
+
* navigation settles as soon as the component is created, so without the interceptor the
|
|
821
|
+
* bar finishes while the page is still empty.
|
|
822
|
+
*
|
|
823
|
+
* @returns Environment providers for the application bootstrap.
|
|
824
|
+
*
|
|
825
|
+
* @example
|
|
826
|
+
* ```typescript
|
|
827
|
+
* bootstrapApplication(AppComponent, {
|
|
828
|
+
* providers: [provideRouter(routes), provideHubLoadingBarRouter()]
|
|
829
|
+
* });
|
|
830
|
+
* ```
|
|
831
|
+
*/
|
|
832
|
+
function provideHubLoadingBarRouter() {
|
|
833
|
+
return makeEnvironmentProviders([
|
|
834
|
+
provideAppInitializer(() => {
|
|
835
|
+
const router = inject(Router);
|
|
836
|
+
const bar = inject(HubLoadingBarService);
|
|
837
|
+
let navigating = false;
|
|
838
|
+
const subscription = router.events.subscribe((event) => {
|
|
839
|
+
if (event instanceof NavigationStart) {
|
|
840
|
+
if (!navigating) {
|
|
841
|
+
navigating = true;
|
|
842
|
+
bar.start();
|
|
843
|
+
}
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (isNavigationSettled(event) && navigating) {
|
|
847
|
+
navigating = false;
|
|
848
|
+
bar.complete();
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
inject(DestroyRef).onDestroy(() => subscription.unsubscribe());
|
|
852
|
+
})
|
|
853
|
+
]);
|
|
854
|
+
}
|
|
855
|
+
|
|
305
856
|
/** Public API surface of ng-hub-ui-loading. */
|
|
306
857
|
|
|
307
858
|
/**
|
|
308
859
|
* Generated bundle index. Do not edit.
|
|
309
860
|
*/
|
|
310
861
|
|
|
311
|
-
export { HUB_LOADING_CONFIG, HUB_LOADING_DEFAULT_CONFIG, HubLoadingComponent, HubLoadingService, provideHubLoading };
|
|
862
|
+
export { HUB_LOADING_BAR_CONFIG, HUB_LOADING_BAR_DEFAULT_CONFIG, HUB_LOADING_BAR_SKIP, HUB_LOADING_CONFIG, HUB_LOADING_DEFAULT_CONFIG, HubLoadingBarComponent, HubLoadingBarService, HubLoadingComponent, HubLoadingService, hubLoadingBarInterceptor, hubLoadingBarTrickle, provideHubLoading, provideHubLoadingBar, provideHubLoadingBarRouter, withoutHubLoadingBar };
|
|
312
863
|
//# sourceMappingURL=ng-hub-ui-loading.mjs.map
|