ng-hub-ui-utils 22.15.3 → 22.18.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 +258 -155
- package/fesm2022/ng-hub-ui-utils.mjs +916 -54
- package/fesm2022/ng-hub-ui-utils.mjs.map +1 -1
- package/package.json +1 -1
- package/styles/documented-style-paths.spec.ts +84 -0
- package/styles/overlay.scss +5 -0
- package/types/ng-hub-ui-utils.d.ts +570 -4
package/README.md
CHANGED
|
@@ -62,6 +62,7 @@ This library is part of the **ng-hub-ui** ecosystem:
|
|
|
62
62
|
- [**ng-hub-ui-portal**](https://www.npmjs.com/package/ng-hub-ui-portal)
|
|
63
63
|
- [**ng-hub-ui-skeleton**](https://www.npmjs.com/package/ng-hub-ui-skeleton)
|
|
64
64
|
- [**ng-hub-ui-sortable**](https://www.npmjs.com/package/ng-hub-ui-sortable)
|
|
65
|
+
- [**ng-hub-ui-spreadsheet**](https://www.npmjs.com/package/ng-hub-ui-spreadsheet)
|
|
65
66
|
- [**ng-hub-ui-stepper**](https://www.npmjs.com/package/ng-hub-ui-stepper)
|
|
66
67
|
- [**ng-hub-ui-utils**](https://www.npmjs.com/package/ng-hub-ui-utils) ← You are here
|
|
67
68
|
|
|
@@ -93,31 +94,34 @@ Advanced system for creating overlays and floating components with flexible posi
|
|
|
93
94
|
import { OverlayService, OverlayConfig } from 'ng-hub-ui-utils';
|
|
94
95
|
|
|
95
96
|
@Component({
|
|
96
|
-
|
|
97
|
+
selector: 'app-example'
|
|
97
98
|
})
|
|
98
99
|
export class ExampleComponent {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
100
|
+
constructor(private overlayService: OverlayService) {}
|
|
101
|
+
|
|
102
|
+
openOverlay(elementRef: ElementRef) {
|
|
103
|
+
// Create overlay with configuration
|
|
104
|
+
const overlayRef = this.overlayService.create({
|
|
105
|
+
hasBackdrop: true,
|
|
106
|
+
backdropClass: 'custom-backdrop'
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Configure position strategy
|
|
110
|
+
const positionStrategy = this.overlayService
|
|
111
|
+
.position()
|
|
112
|
+
.flexibleConnectedTo(elementRef)
|
|
113
|
+
.withPositions([
|
|
114
|
+
{
|
|
115
|
+
originX: 'start',
|
|
116
|
+
originY: 'bottom',
|
|
117
|
+
overlayX: 'start',
|
|
118
|
+
overlayY: 'top'
|
|
119
|
+
}
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
// Render a component (or a TemplateRef) into the overlay; you get the host element back
|
|
123
|
+
const overlayElement = overlayRef.attach(MyComponent);
|
|
124
|
+
}
|
|
121
125
|
}
|
|
122
126
|
```
|
|
123
127
|
|
|
@@ -138,14 +142,14 @@ import { PopupService } from 'ng-hub-ui-utils';
|
|
|
138
142
|
|
|
139
143
|
@Injectable()
|
|
140
144
|
export class MyPopupService extends PopupService<MyPopupComponent> {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
145
|
+
constructor() {
|
|
146
|
+
super(MyPopupComponent);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
openPopup(content?: string | TemplateRef<any>) {
|
|
150
|
+
const { windowRef, transition$ } = super.open(content, {}, true);
|
|
151
|
+
return { windowRef, transition$ };
|
|
152
|
+
}
|
|
149
153
|
}
|
|
150
154
|
```
|
|
151
155
|
|
|
@@ -248,14 +252,14 @@ import { GetPipe, IsStringPipe, IsObjectPipe, IsObservablePipe, UcfirstPipe, Unw
|
|
|
248
252
|
imports: [GetPipe, IsStringPipe, UcfirstPipe, UnwrapAsyncPipe],
|
|
249
253
|
template: `
|
|
250
254
|
<!-- Safe nested property access -->
|
|
251
|
-
<p>{{ user | get
|
|
255
|
+
<p>{{ user | get: 'address.city' : 'Unknown' }}</p>
|
|
252
256
|
|
|
253
257
|
<!-- Capitalize first letter -->
|
|
254
258
|
<h1>{{ title | ucfirst }}</h1>
|
|
255
259
|
|
|
256
260
|
<!-- Type checking in templates -->
|
|
257
261
|
@if (value | isString) {
|
|
258
|
-
|
|
262
|
+
<span>It's a string: {{ value }}</span>
|
|
259
263
|
}
|
|
260
264
|
|
|
261
265
|
<!-- Unwrap Observable or direct value -->
|
|
@@ -272,12 +276,12 @@ export class ExampleComponent {
|
|
|
272
276
|
|
|
273
277
|
**Available Pipes:**
|
|
274
278
|
|
|
275
|
-
-
|
|
276
|
-
-
|
|
277
|
-
-
|
|
278
|
-
-
|
|
279
|
-
-
|
|
280
|
-
-
|
|
279
|
+
- **GetPipe** (`get`): Safe nested property access with default values
|
|
280
|
+
- **IsStringPipe** (`isString`): Check if value is a string
|
|
281
|
+
- **IsObjectPipe** (`isObject`): Check if value is an object
|
|
282
|
+
- **IsObservablePipe** (`isObservable`): Check if value is an Observable
|
|
283
|
+
- **UcfirstPipe** (`ucfirst`): Capitalize first letter of a string
|
|
284
|
+
- **UnwrapAsyncPipe** (`unwrapAsync`): Unwrap Observable or return direct value
|
|
281
285
|
|
|
282
286
|
### 🛠️ General Utility Functions
|
|
283
287
|
|
|
@@ -383,6 +387,19 @@ export class ExampleComponent {}
|
|
|
383
387
|
Inputs: `hubTooltip` (text), `hubTooltipPlacement` (`top` | `bottom` | `left` | `right`,
|
|
384
388
|
default `top`), `hubTooltipDelay` (fade ms, default `150`), `hubTooltipOffset` (px, default `8`).
|
|
385
389
|
|
|
390
|
+
**The placement is a preference, not an instruction (since 22.16.0).** A tooltip that would open
|
|
391
|
+
off the edge of the window opens on the opposite side instead, at all four edges, and one centred
|
|
392
|
+
on a host near the inline edge is slid back inside rather than flipped — flipping does nothing for
|
|
393
|
+
an overflow on the cross axis. It gives way only when it genuinely has no room, so a tooltip that
|
|
394
|
+
did not need to move does not, and the fit is re-run while the label is open, on window resize and
|
|
395
|
+
on a scroll anywhere above it. Under RTL the inline edge is the other one and the flip follows it,
|
|
396
|
+
while `hubTooltipPlacement="left"` still means the host's left edge.
|
|
397
|
+
|
|
398
|
+
If you have been writing `hubTooltipPlacement="bottom"` by hand on every hint in a header, you can
|
|
399
|
+
stop: the attribute is still honoured wherever it fits, but it is no longer what keeps the label
|
|
400
|
+
on screen. The class on the bubble (`hub-tooltip--top`, `--bottom`, `--left`, `--right`) names the
|
|
401
|
+
side it ended up on, so an arrow styled off it follows the flip.
|
|
402
|
+
|
|
386
403
|
> **`TooltipDirective` (`[tooltip]`) was removed in 22.14.0**, having been deprecated since
|
|
387
404
|
> 22.9.0. Its bare input names (`tooltip`, `placement`, `delay`, `offset`) belonged to every
|
|
388
405
|
> directive on the element that declared them, which is how it collided with `[hubDropdown]`'s
|
|
@@ -449,10 +466,7 @@ import { hubTooltipAdapter } from 'ng-hub-ui-utils';
|
|
|
449
466
|
import { provideHubBadgeTooltip } from 'ng-hub-ui-badges';
|
|
450
467
|
import { provideHubBreadcrumbTooltip } from 'ng-hub-ui-breadcrumbs';
|
|
451
468
|
|
|
452
|
-
providers: [
|
|
453
|
-
provideHubBadgeTooltip(hubTooltipAdapter),
|
|
454
|
-
provideHubBreadcrumbTooltip(hubTooltipAdapter)
|
|
455
|
-
];
|
|
469
|
+
providers: [provideHubBadgeTooltip(hubTooltipAdapter), provideHubBreadcrumbTooltip(hubTooltipAdapter)];
|
|
456
470
|
```
|
|
457
471
|
|
|
458
472
|
Inside this package the same token works the other way round: `[hubOverflowTooltip]`
|
|
@@ -476,6 +490,55 @@ Also available: the imperative `HubTooltipController` (engine) and the
|
|
|
476
490
|
types. See the ecosystem-wide
|
|
477
491
|
[Synergies & agnosticism](../../README.md#synergies--agnosticism) section.
|
|
478
492
|
|
|
493
|
+
### 🔢 Grid Primitives
|
|
494
|
+
|
|
495
|
+
The arithmetic a sheet of cells is made of, with no DOM and no framework in it. A grid has a
|
|
496
|
+
handful of problems every implementation solves again and gets wrong in the same places: where the
|
|
497
|
+
cursor goes at an edge, what rectangle two corners describe, which cell a merged block swallowed,
|
|
498
|
+
and which tracks are worth drawing at all. `ng-hub-ui-spreadsheet` is built on these, which is how
|
|
499
|
+
it installs without `@angular/cdk`.
|
|
500
|
+
|
|
501
|
+
```ts
|
|
502
|
+
import {
|
|
503
|
+
resolveGridIntent,
|
|
504
|
+
moveGridFocus,
|
|
505
|
+
gridRangeBetween,
|
|
506
|
+
buildSpanMap,
|
|
507
|
+
anchorOf,
|
|
508
|
+
gridWindow,
|
|
509
|
+
growWindowToSpans
|
|
510
|
+
} from 'ng-hub-ui-utils';
|
|
511
|
+
|
|
512
|
+
// A key read as an intent, so the grid does not grow a switch statement per browser. The
|
|
513
|
+
// roving tab stop is `gridCellTabIndex(cell, active, 'roving')`.
|
|
514
|
+
const intent = resolveGridIntent(event, { pageSize: 20 });
|
|
515
|
+
|
|
516
|
+
// The cursor, with the three different answers at the end of a row told apart.
|
|
517
|
+
const next = moveGridFocus(cursor, { row: 0, col: 1 }, bounds, { horizontal: 'continuous' });
|
|
518
|
+
|
|
519
|
+
// A selection kept as two corners, so ten thousand cells cost nothing.
|
|
520
|
+
const range = gridRangeBetween(anchor, cursor);
|
|
521
|
+
|
|
522
|
+
// Merged blocks: the cursor lands on the block and never inside it.
|
|
523
|
+
const spans = buildSpanMap([{ row: 4, col: 0, rowSpan: 3, colSpan: 2 }]);
|
|
524
|
+
const landing = anchorOf(spans, cursor);
|
|
525
|
+
|
|
526
|
+
// Only what the viewport covers, with the pixels the rest would have taken, so the scrollbar
|
|
527
|
+
// still tells the truth. Then widened until every block it touches is drawn whole.
|
|
528
|
+
const window = growWindowToSpans(
|
|
529
|
+
gridWindow({ offset: scrollTop, viewport: height, sizes: 32, count: rows.length, pinned: 1 }),
|
|
530
|
+
blocks,
|
|
531
|
+
'row',
|
|
532
|
+
32,
|
|
533
|
+
{ count: rows.length, pinned: 1 }
|
|
534
|
+
);
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
Also exported: `resolveGridEdge`, `isWithinGridRange`, `gridRangeCells`, `gridRangeSize`,
|
|
538
|
+
`clampGridRange`, `spanAt`, `isCovered`, `coversRange`, `gridCellTabIndex`, and the types
|
|
539
|
+
`HubGridCoords`, `HubGridBounds`, `HubGridRange`, `HubGridSpan`, `HubGridSpanMap`, `HubGridWrap`,
|
|
540
|
+
`HubGridEdge`, `HubGridIntent`, `HubGridWindow` and `HubGridTrackSizes`.
|
|
541
|
+
|
|
479
542
|
## 🚀 Installation
|
|
480
543
|
|
|
481
544
|
```bash
|
|
@@ -497,7 +560,7 @@ import { toInteger, isString, ScrollBar, getFocusableBoundaryElements, GetPipe,
|
|
|
497
560
|
template: `
|
|
498
561
|
<div #container>
|
|
499
562
|
<h1>{{ title | ucfirst }}</h1>
|
|
500
|
-
<p>{{ user | get
|
|
563
|
+
<p>{{ user | get: 'name' : 'Anonymous' }}</p>
|
|
501
564
|
</div>
|
|
502
565
|
`
|
|
503
566
|
})
|
|
@@ -598,7 +661,7 @@ class HubTranslationService {
|
|
|
598
661
|
```typescript
|
|
599
662
|
import { HubTranslationService } from 'ng-hub-ui-utils';
|
|
600
663
|
|
|
601
|
-
@Component({
|
|
664
|
+
@Component({/* ... */})
|
|
602
665
|
export class LanguageSwitcherComponent {
|
|
603
666
|
private translationSvc = inject(HubTranslationService);
|
|
604
667
|
|
|
@@ -638,81 +701,81 @@ These functions back the i18n system and are exported for direct use:
|
|
|
638
701
|
|
|
639
702
|
### Conversion Functions
|
|
640
703
|
|
|
641
|
-
-
|
|
642
|
-
-
|
|
643
|
-
-
|
|
644
|
-
-
|
|
704
|
+
- `toInteger(value: any): number` - Safely converts to integer
|
|
705
|
+
- `toString(value: any): string` - Converts to string handling null/undefined
|
|
706
|
+
- `getValueInRange(value: number, max: number, min?: number): number` - Limits value to range
|
|
707
|
+
- `padNumber(value: number): string` - Adds leading zero to numbers
|
|
645
708
|
|
|
646
709
|
### Validation Functions
|
|
647
710
|
|
|
648
|
-
-
|
|
649
|
-
-
|
|
650
|
-
-
|
|
651
|
-
-
|
|
652
|
-
-
|
|
711
|
+
- `isString(value: any): value is string` - Checks if value is a string
|
|
712
|
+
- `isNumber(value: any): value is number` - Checks if value is a valid number
|
|
713
|
+
- `isInteger(value: any): value is number` - Checks if value is an integer
|
|
714
|
+
- `isDefined(value: any): boolean` - Checks if not null/undefined
|
|
715
|
+
- `isPromise<T>(v: any): v is Promise<T>` - Checks if value is a Promise
|
|
653
716
|
|
|
654
717
|
### String Functions
|
|
655
718
|
|
|
656
|
-
-
|
|
657
|
-
-
|
|
658
|
-
-
|
|
659
|
-
-
|
|
719
|
+
- `regExpEscape(text: string): string` - Escapes special characters for RegExp
|
|
720
|
+
- `removeAccents(str: string): string` - Removes accents from text
|
|
721
|
+
- `interpolateString(expr?: string, params?: any, templateMatcher?: RegExp): string` - Replaces `{{ token }}` placeholders
|
|
722
|
+
- `generateUniqueId(length: number): string` - Random alphanumeric id, for a DOM node that needs one
|
|
660
723
|
|
|
661
724
|
### Object Functions
|
|
662
725
|
|
|
663
|
-
-
|
|
664
|
-
-
|
|
665
|
-
-
|
|
666
|
-
-
|
|
726
|
+
- `equals(o1: any, o2: any): boolean` - Deep equality
|
|
727
|
+
- `getValue(target: any, key: string): any` - Reads a nested value by dot-notation key
|
|
728
|
+
- `isObject(item: any): boolean` - Whether the value is a non-array object
|
|
729
|
+
- `mergeDeep(target: any, source: any): any` - Recursive merge; the only deep object helper in the package
|
|
667
730
|
|
|
668
731
|
### Signal Utilities
|
|
669
732
|
|
|
670
|
-
-
|
|
733
|
+
- `debouncedSignal<T>(source: Signal<T>, delay?: number | Signal<number>): Signal<T>` - Mirrors a signal, delaying each change; the delay can itself be a signal
|
|
671
734
|
|
|
672
735
|
### DOM Functions
|
|
673
736
|
|
|
674
|
-
-
|
|
675
|
-
-
|
|
676
|
-
-
|
|
737
|
+
- `closest(element: HTMLElement, selector?: string): HTMLElement | null` - Finds parent element by selector
|
|
738
|
+
- `reflow(element: HTMLElement): DOMRect` - Forces browser reflow
|
|
739
|
+
- `getActiveElement(root?: Document | ShadowRoot): Element | null` - Gets active element including Shadow DOM
|
|
677
740
|
|
|
678
741
|
### Accent Resolution
|
|
679
742
|
|
|
680
|
-
-
|
|
743
|
+
- `resolveHubAccent(value: string | null | undefined): string | null` - The "any colour" accent resolver shared across the family: a bareword becomes `var(--hub-sys-color-<name>, <name>)`, a literal `#hex` / `rgb()` / `oklch()` / `var()` passes through unchanged, and an empty value yields `null`
|
|
681
744
|
|
|
682
745
|
### Colour Functions
|
|
683
746
|
|
|
684
|
-
-
|
|
685
|
-
-
|
|
686
|
-
-
|
|
687
|
-
-
|
|
688
|
-
-
|
|
747
|
+
- `parseColor(value): HubRgb | null` - Parses hex (3/4/6/8), `rgb()`, `hsl()`, `oklch()`, `oklab()`, the 148 CSS named colours and `transparent`, in modern and legacy syntax. No DOM, so it runs under SSR. Returns `null` — never throws — for anything it cannot resolve, `var()` and `currentColor` included
|
|
748
|
+
- `toRgb(color): HubRgb | null` - Normalises a string or parsed colour to channels
|
|
749
|
+
- `toHex(color): string | null` - Renders as `#rrggbb`, or `#rrggbbaa` when translucent
|
|
750
|
+
- `isValidColor(value): boolean` - Whether the parser can resolve the string
|
|
751
|
+
- `HUB_NAMED_COLORS: Readonly<Record<string, string>>` - The 148 CSS named colours
|
|
689
752
|
|
|
690
753
|
### Contrast Functions
|
|
691
754
|
|
|
692
|
-
-
|
|
693
|
-
-
|
|
694
|
-
-
|
|
695
|
-
-
|
|
696
|
-
-
|
|
697
|
-
-
|
|
755
|
+
- `relativeLuminance(color): number | null` - WCAG 2 relative luminance, 0 to 1
|
|
756
|
+
- `contrastRatio(a, b): number | null` - WCAG 2 contrast ratio, 1 to 21
|
|
757
|
+
- `contrastAPCA(text, background): number | null` - APCA lightness contrast, polarity-aware
|
|
758
|
+
- `compositeOver(foreground, background): HubColor` - Blends translucent over opaque
|
|
759
|
+
- `readableOn(background, metric?): string` - Black or white, whichever reads better. Defaults to `'lightness'`, the same decision `--hub-sys-color-*-on` makes in CSS; `'apca'` and `'wcag'` are also available
|
|
760
|
+
- `HUB_INK_LIGHTNESS_THRESHOLD: number` - The OKLCh lightness above which a surface takes dark ink
|
|
698
761
|
|
|
699
762
|
### OKLCh Functions
|
|
700
763
|
|
|
701
|
-
-
|
|
702
|
-
-
|
|
703
|
-
-
|
|
704
|
-
-
|
|
764
|
+
- `rgbToOklch(color): HubOklch` / `oklchToRgb(color): HubRgb` - Conversions in the space the design system mixes in
|
|
765
|
+
- `maxSrgbChroma(l, h): number` - Highest in-gamut chroma for a hue at a lightness. The sRGB gamut is not a cylinder — at L 0.578 blue reaches 0.232 and amber only 0.119 — so a palette cannot give every hue the same absolute chroma
|
|
766
|
+
- `isInSrgbGamut(color): boolean` - Whether the colour survives the trip to sRGB
|
|
767
|
+
- `clampToSrgbGamut(color): HubOklch` - Reduces chroma until it fits, preserving lightness and hue
|
|
705
768
|
|
|
706
769
|
### Palette Derivation
|
|
707
770
|
|
|
708
771
|
One brand colour, the whole palette — and the two numbers that keep it honest.
|
|
709
772
|
|
|
710
|
-
-
|
|
711
|
-
-
|
|
712
|
-
-
|
|
713
|
-
-
|
|
714
|
-
-
|
|
715
|
-
-
|
|
773
|
+
- `harmoniseSemantics(primary, options?): HubSemanticPalette | null` - Rotates `success`, `warning`, `danger` and `info` towards the brand's hue and returns them as hex. Lightness is left exactly where the anchor had it, because it is what carries the contrast each role was chosen for; chroma is reduced only when the new hue cannot hold it inside sRGB
|
|
774
|
+
- `tintNeutrals(primary, options?): HubNeutralRamp | null` - Leans the grey ramp (`100` … `900`) the same way, keeping each step's lightness
|
|
775
|
+
- `HUB_MAX_HUE_SHIFT: number` (15) - How far a role may rotate, in degrees. Not taste: success and danger sit 135.8° apart in OKLCh, and a viewer with deuteranopia separates them by hue alone. A brand hue between the two pulls both inwards, so the gap closes by up to twice the cap; at 22.9° it would reach the 90° floor. 15° leaves the worst case at 105.8°
|
|
776
|
+
- `HUB_MAX_NEUTRAL_CHROMA: number` (0.015) - The most chroma a tinted neutral may carry. Anchored on the ramp the design system already ships — `gray-600` measures 0.0165 and `gray-500` 0.0145 — so a tinted ramp is never more colourful than the grey people already accept as grey
|
|
777
|
+
- `HUB_SEMANTIC_ANCHORS` / `HUB_NEUTRAL_ANCHORS` - The untinted starting points, so a product that harmonises nothing still gets the palette the stylesheet ships
|
|
778
|
+
- Types: `HubSemanticRole`, `HubSemanticPalette`, `HubNeutralStep`, `HubNeutralRamp`, `HubHarmoniseOptions`, `HubTintNeutralsOptions`
|
|
716
779
|
|
|
717
780
|
```typescript
|
|
718
781
|
import { harmoniseSemantics, tintNeutrals } from 'ng-hub-ui-utils';
|
|
@@ -728,9 +791,9 @@ is rounding noise, and harmonising towards it would rotate every role in a direc
|
|
|
728
791
|
|
|
729
792
|
### Focus Functions
|
|
730
793
|
|
|
731
|
-
-
|
|
732
|
-
-
|
|
733
|
-
-
|
|
794
|
+
- `getFocusableBoundaryElements(element: HTMLElement): HTMLElement[]` - Gets first and last focusable elements
|
|
795
|
+
- `hubFocusTrap(zone, element, stopFocusTrap$, refocusOnClick?)` - Creates focus trap for modals/overlays
|
|
796
|
+
- `FOCUSABLE_ELEMENTS_SELECTOR: string` - CSS selector for focusable elements
|
|
734
797
|
|
|
735
798
|
### Drag and Drop
|
|
736
799
|
|
|
@@ -738,19 +801,19 @@ The engine-agnostic half of native HTML5 drag and drop, shared by the libraries
|
|
|
738
801
|
implement it. The UI primitives — handle, placeholder and preview directives — stay in each
|
|
739
802
|
library, because their selectors and data models differ.
|
|
740
803
|
|
|
741
|
-
-
|
|
742
|
-
-
|
|
743
|
-
-
|
|
744
|
-
-
|
|
745
|
-
-
|
|
746
|
-
-
|
|
747
|
-
-
|
|
804
|
+
- `HubDragDropService` - Root-provided coordinator. A drag spans two component instances and the native `dataTransfer` payload is unreadable during `dragover`, so a shared service is the only reliable channel for what is being dragged and from where. Owners `register()` / `unregister()`; `begin()`, `setTarget()` and the readonly `active` / `target` / `isDragging` signals report the drag in progress. It coordinates state only — it never mutates your collections
|
|
805
|
+
- `moveItemInArray<T>(array, fromIndex, toIndex): void` / `transferArrayItem<T>(source, target, fromIndex, toIndex): void` / `copyArrayItem<T>(source, target, fromIndex, toIndex): void` - In-place array moves, mirroring the `@angular/cdk` helpers of the same names
|
|
806
|
+
- `clamp(value, max)`, `computeTargetIndex(...)`, `toAbsoluteIndex(...)`, `containsNode(...)` - Index arithmetic for sliced and nested lists
|
|
807
|
+
- `resolveDropPosition(...)` with `DropRect` and `DragAxis` - Where a pointer sits relative to an item: `'before'` or `'after'`, on a vertical, horizontal or grid axis
|
|
808
|
+
- `createNativeDragImage(...)` returning `DragImageResult` - Renders the drag preview the browser shows
|
|
809
|
+
- `createPointerDragSession(config: PointerDragSessionConfig): PointerDragSession` - Pointer Events fallback for touch, where native drag events are not delivered
|
|
810
|
+
- Types: `DropPosition`, `DragPointerMode`, `DragContainerRef<T>`, `ActiveDrag<T>`, `DragTarget<T>`, `DragRegistration`
|
|
748
811
|
|
|
749
812
|
### Directives
|
|
750
813
|
|
|
751
|
-
-
|
|
752
|
-
-
|
|
753
|
-
-
|
|
814
|
+
- `HubTooltipDirective` (`[hubTooltip]`) - Tooltip on hover/focus, flipped away from the window edges. Inputs: `hubTooltip`, `hubTooltipPlacement`, `hubTooltipDelay`, `hubTooltipOffset`
|
|
815
|
+
- `HubOverflowTooltipDirective` (`[hubOverflowTooltip]`) - Tooltip shown only while the label is truncated. Inputs: `hubOverflowTooltip`, `placement`, `hubOverflowTooltipMeasure` (CSS selector, resolved inside the host, naming the box whose truncation decides it; defaults to the host)
|
|
816
|
+
- `provideHubTooltip(adapter: HubTooltipAdapter)` and `HUB_TOOLTIP_ADAPTER` - Swap the implementation behind `[hubOverflowTooltip]`, app-wide or per subtree; defaults to `hubTooltipAdapter`
|
|
754
817
|
|
|
755
818
|
### Pipes
|
|
756
819
|
|
|
@@ -807,31 +870,31 @@ library, because their selectors and data models differ.
|
|
|
807
870
|
```typescript
|
|
808
871
|
@Injectable({ providedIn: 'root' })
|
|
809
872
|
class OverlayService {
|
|
810
|
-
|
|
811
|
-
|
|
873
|
+
create(config?: OverlayConfig): OverlayRef;
|
|
874
|
+
position(): OverlayPosition;
|
|
812
875
|
}
|
|
813
876
|
|
|
814
877
|
class OverlayRef {
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
878
|
+
// Renders a template or a component into the overlay and returns the host element,
|
|
879
|
+
// not a ComponentRef: the overlay owns the view it created and tears it down itself.
|
|
880
|
+
attach(content: TemplateRef<unknown> | Type<unknown>, viewContainerRef?: ViewContainerRef): HTMLElement;
|
|
881
|
+
detach(): void;
|
|
882
|
+
dispose(): void;
|
|
883
|
+
hasAttached(): boolean;
|
|
884
|
+
updatePosition(): void;
|
|
885
|
+
onBackdropClick(callback: () => void): void;
|
|
886
|
+
// Only the topmost open overlay is told, so a dropdown inside a dialog takes Escape
|
|
887
|
+
// for itself and leaves the dialog open.
|
|
888
|
+
onKeydown(callback: (event: KeyboardEvent) => void): void;
|
|
826
889
|
}
|
|
827
890
|
|
|
828
891
|
class OverlayPosition {
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
892
|
+
// The element the panel is anchored to. The overlay watches it and follows it when it moves.
|
|
893
|
+
readonly origin: HTMLElement | null;
|
|
894
|
+
flexibleConnectedTo(origin: ElementRef | HTMLElement): this;
|
|
895
|
+
withPositions(positions: ConnectionPosition[]): this;
|
|
896
|
+
// `start` / `end` are logical and read from the origin element; this overrides that.
|
|
897
|
+
withDirection(direction: 'ltr' | 'rtl' | null): this;
|
|
835
898
|
}
|
|
836
899
|
```
|
|
837
900
|
|
|
@@ -842,15 +905,55 @@ both text directions:
|
|
|
842
905
|
```typescript
|
|
843
906
|
import { HUB_DROPDOWN_POSITIONS } from 'ng-hub-ui-utils';
|
|
844
907
|
|
|
845
|
-
overlayService
|
|
908
|
+
overlayService
|
|
909
|
+
.position()
|
|
910
|
+
.flexibleConnectedTo(origin)
|
|
911
|
+
.withPositions([...HUB_DROPDOWN_POSITIONS]);
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
#### Viewport fitting (`viewport-fit`)
|
|
915
|
+
|
|
916
|
+
The arithmetic behind both the overlay strategy and the tooltip, exported on its own so a floating
|
|
917
|
+
element that is neither does not have to write a third copy. It takes plain numbers, touches no DOM
|
|
918
|
+
and runs unchanged on a server render.
|
|
919
|
+
|
|
920
|
+
```typescript
|
|
921
|
+
import { hubAnchorToViewport, hubToAnchorSide, hubToPhysicalSide, hubViewportOf } from 'ng-hub-ui-utils';
|
|
922
|
+
|
|
923
|
+
const rtl = getComputedStyle(host).direction === 'rtl';
|
|
924
|
+
|
|
925
|
+
const placed = hubAnchorToViewport({
|
|
926
|
+
anchor: host.getBoundingClientRect(),
|
|
927
|
+
box: { width: panel.offsetWidth, height: panel.offsetHeight },
|
|
928
|
+
viewport: hubViewportOf(panel), // null on a server render: nothing flips
|
|
929
|
+
side: hubToAnchorSide('bottom', rtl), // 'block-start' | 'block-end' | 'inline-start' | 'inline-end'
|
|
930
|
+
align: 'start', // logical: mirrors under RTL
|
|
931
|
+
offset: 8,
|
|
932
|
+
margin: 0,
|
|
933
|
+
rtl
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
// placed.x / placed.y are viewport coordinates — add scrollX / scrollY for an absolutely
|
|
937
|
+
// positioned element. placed.side is where it ended up, placed.flipped whether that was a flip.
|
|
938
|
+
panel.dataset['side'] = hubToPhysicalSide(placed.side, rtl);
|
|
846
939
|
```
|
|
847
940
|
|
|
941
|
+
The requested side is kept unless it genuinely cannot hold the box **and** the opposite side has
|
|
942
|
+
more room, so nothing flips for free; the cross axis is clamped rather than flipped, since a box
|
|
943
|
+
centred on an anchor near the inline edge overflows there whichever side it opens on. Sides are
|
|
944
|
+
logical so a single fallback rule serves both text directions — `hubToAnchorSide` and
|
|
945
|
+
`hubToPhysicalSide` round-trip a physical edge through that axis without changing what the caller
|
|
946
|
+
asked for.
|
|
947
|
+
|
|
948
|
+
Also exported: `hubFitsInViewport(point, size, viewport, margin?)`,
|
|
949
|
+
`hubClampToViewport(point, size, viewport, margin?)` and `hubOppositeSide(side)`.
|
|
950
|
+
|
|
848
951
|
#### ScrollBar Service
|
|
849
952
|
|
|
850
953
|
```typescript
|
|
851
954
|
@Injectable({ providedIn: 'root' })
|
|
852
955
|
class ScrollBar {
|
|
853
|
-
|
|
956
|
+
hide(): ScrollbarReverter; // Hides scrollbar with compensation
|
|
854
957
|
}
|
|
855
958
|
```
|
|
856
959
|
|
|
@@ -862,48 +965,48 @@ an injection context — as an `@Injectable()` subclass, or from a factory provi
|
|
|
862
965
|
|
|
863
966
|
```typescript
|
|
864
967
|
class PopupService<T> {
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
968
|
+
constructor(componentType: Type<T>);
|
|
969
|
+
open(
|
|
970
|
+
content?: string | TemplateRef<any>,
|
|
971
|
+
templateContext?: any,
|
|
972
|
+
animation?: boolean
|
|
973
|
+
): { windowRef: ComponentRef<T>; transition$: Observable<void> };
|
|
974
|
+
close(animation?: boolean): Observable<void>;
|
|
872
975
|
}
|
|
873
976
|
|
|
874
977
|
// The nodes and view a popup projects, returned internally by the content resolver.
|
|
875
978
|
class ContentRef {
|
|
876
|
-
|
|
979
|
+
constructor(nodes: Node[][], viewRef?: ViewRef, componentRef?: ComponentRef<any>);
|
|
877
980
|
}
|
|
878
981
|
```
|
|
879
982
|
|
|
880
983
|
### Transition Utilities
|
|
881
984
|
|
|
882
|
-
-
|
|
883
|
-
-
|
|
884
|
-
-
|
|
885
|
-
-
|
|
985
|
+
- `hubRunTransition<T>(zone, element, startFn, options)` - Advanced transition system with Observable
|
|
986
|
+
- `hubCompleteTransition(element)` - Completes a running transition on an element
|
|
987
|
+
- `getTransitionDurationMs(element)` - Gets CSS transition duration in milliseconds
|
|
988
|
+
- `runInZone<T>(zone)` - RxJS operator to execute observables inside NgZone
|
|
886
989
|
|
|
887
990
|
## 🎨 Support Components
|
|
888
991
|
|
|
889
992
|
This library doesn't include visual components, but support utilities used by other components in the Hub UI ecosystem:
|
|
890
993
|
|
|
891
|
-
| Utility | Description | Used by
|
|
892
|
-
| --------------- | ----------------------------------- |
|
|
893
|
-
| Overlay Service | Flexible overlay positioning system | ng-hub-ui-modal, ng-hub-ui-portal
|
|
894
|
-
| Focus Trap | Focus management in modals/overlays | ng-hub-ui-modal, ng-hub-ui-portal
|
|
895
|
-
| Scrollbar | Scrollbar compensation | ng-hub-ui-modal, ng-hub-ui-portal
|
|
896
|
-
| Popup Service | Host for dynamically created popups | ng-hub-ui-modal, ng-hub-ui-portal
|
|
897
|
-
| Transitions | Smooth animations | ng-hub-ui-accordion, ng-hub-ui-modal
|
|
898
|
-
| Type Guards | Type validation functions | ng-hub-ui-stepper
|
|
899
|
-
| Pipes | Template utilities | All Hub UI components
|
|
994
|
+
| Utility | Description | Used by |
|
|
995
|
+
| --------------- | ----------------------------------- | ------------------------------------ |
|
|
996
|
+
| Overlay Service | Flexible overlay positioning system | ng-hub-ui-modal, ng-hub-ui-portal |
|
|
997
|
+
| Focus Trap | Focus management in modals/overlays | ng-hub-ui-modal, ng-hub-ui-portal |
|
|
998
|
+
| Scrollbar | Scrollbar compensation | ng-hub-ui-modal, ng-hub-ui-portal |
|
|
999
|
+
| Popup Service | Host for dynamically created popups | ng-hub-ui-modal, ng-hub-ui-portal |
|
|
1000
|
+
| Transitions | Smooth animations | ng-hub-ui-accordion, ng-hub-ui-modal |
|
|
1001
|
+
| Type Guards | Type validation functions | ng-hub-ui-stepper |
|
|
1002
|
+
| Pipes | Template utilities | All Hub UI components |
|
|
900
1003
|
|
|
901
1004
|
## 🤝 Compatibility
|
|
902
1005
|
|
|
903
|
-
-
|
|
904
|
-
-
|
|
905
|
-
-
|
|
906
|
-
-
|
|
1006
|
+
- Angular 16+
|
|
1007
|
+
- TypeScript 4.8+
|
|
1008
|
+
- Node.js 16+
|
|
1009
|
+
- Browsers: Chrome 90+, Firefox 88+, Safari 14+, Edge 90+
|
|
907
1010
|
|
|
908
1011
|
## 🛠️ Development
|
|
909
1012
|
|
|
@@ -977,10 +1080,10 @@ Recent highlights:
|
|
|
977
1080
|
|
|
978
1081
|
## 🐛 Issues and Support
|
|
979
1082
|
|
|
980
|
-
-
|
|
981
|
-
-
|
|
982
|
-
-
|
|
983
|
-
-
|
|
1083
|
+
- [Report a bug](https://github.com/hub-env/hub-ui/issues)
|
|
1084
|
+
- [Request a feature](https://github.com/hub-env/hub-ui/issues/new?template=feature_request.yml)
|
|
1085
|
+
- [Repository](https://github.com/hub-env/ng-hub-ui-utils)
|
|
1086
|
+
- **Author**: [Carlos Morcillo](https://www.carlosmorcillo.com)
|
|
984
1087
|
|
|
985
1088
|
## ☕ Support the Project
|
|
986
1089
|
|
|
@@ -991,10 +1094,10 @@ If Hub UI has been useful to you, consider supporting its development:
|
|
|
991
1094
|
|
|
992
1095
|
Your support helps to:
|
|
993
1096
|
|
|
994
|
-
-
|
|
995
|
-
-
|
|
996
|
-
-
|
|
997
|
-
-
|
|
1097
|
+
- 🚀 Keep the project active
|
|
1098
|
+
- 🐛 Fix bugs faster
|
|
1099
|
+
- ✨ Develop new features
|
|
1100
|
+
- 📚 Improve documentation
|
|
998
1101
|
|
|
999
1102
|
## 🤝 Contributions
|
|
1000
1103
|
|