@raintonic/formaui 0.2.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/CHANGELOG.md +7 -0
- package/README.md +145 -0
- package/fesm2022/raintonic-formaui-cdk-drag-drop.mjs +806 -0
- package/fesm2022/raintonic-formaui-cdk-drag-drop.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-cdk-form-field.mjs +86 -0
- package/fesm2022/raintonic-formaui-cdk-form-field.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-cdk-overlay.mjs +1757 -0
- package/fesm2022/raintonic-formaui-cdk-overlay.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-cdk-virtual-scroll.mjs +287 -0
- package/fesm2022/raintonic-formaui-cdk-virtual-scroll.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-accordion.mjs +217 -0
- package/fesm2022/raintonic-formaui-components-accordion.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-alert.mjs +161 -0
- package/fesm2022/raintonic-formaui-components-alert.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-autocomplete.mjs +726 -0
- package/fesm2022/raintonic-formaui-components-autocomplete.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-avatar.mjs +92 -0
- package/fesm2022/raintonic-formaui-components-avatar.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-badge.mjs +107 -0
- package/fesm2022/raintonic-formaui-components-badge.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-big-menu.mjs +68 -0
- package/fesm2022/raintonic-formaui-components-big-menu.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-breadcrumb.mjs +55 -0
- package/fesm2022/raintonic-formaui-components-breadcrumb.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-button-group.mjs +103 -0
- package/fesm2022/raintonic-formaui-components-button-group.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-button.mjs +241 -0
- package/fesm2022/raintonic-formaui-components-button.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-card.mjs +270 -0
- package/fesm2022/raintonic-formaui-components-card.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-checkbox.mjs +295 -0
- package/fesm2022/raintonic-formaui-components-checkbox.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-data-table.mjs +631 -0
- package/fesm2022/raintonic-formaui-components-data-table.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-date-picker.mjs +1331 -0
- package/fesm2022/raintonic-formaui-components-date-picker.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-divider.mjs +41 -0
- package/fesm2022/raintonic-formaui-components-divider.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-drawer.mjs +190 -0
- package/fesm2022/raintonic-formaui-components-drawer.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-dynamic-form.mjs +266 -0
- package/fesm2022/raintonic-formaui-components-dynamic-form.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-empty-state.mjs +33 -0
- package/fesm2022/raintonic-formaui-components-empty-state.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-file-upload.mjs +246 -0
- package/fesm2022/raintonic-formaui-components-file-upload.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-form-field.mjs +482 -0
- package/fesm2022/raintonic-formaui-components-form-field.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-icon.mjs +117 -0
- package/fesm2022/raintonic-formaui-components-icon.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-input.mjs +327 -0
- package/fesm2022/raintonic-formaui-components-input.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-list.mjs +149 -0
- package/fesm2022/raintonic-formaui-components-list.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-menu.mjs +896 -0
- package/fesm2022/raintonic-formaui-components-menu.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-number-input.mjs +345 -0
- package/fesm2022/raintonic-formaui-components-number-input.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-paginator.mjs +139 -0
- package/fesm2022/raintonic-formaui-components-paginator.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-password-input.mjs +306 -0
- package/fesm2022/raintonic-formaui-components-password-input.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-popover.mjs +451 -0
- package/fesm2022/raintonic-formaui-components-popover.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-progressbar.mjs +148 -0
- package/fesm2022/raintonic-formaui-components-progressbar.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-radio.mjs +260 -0
- package/fesm2022/raintonic-formaui-components-radio.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-select.mjs +1011 -0
- package/fesm2022/raintonic-formaui-components-select.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-side-panel.mjs +150 -0
- package/fesm2022/raintonic-formaui-components-side-panel.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-sidebar.mjs +257 -0
- package/fesm2022/raintonic-formaui-components-sidebar.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-skeleton.mjs +50 -0
- package/fesm2022/raintonic-formaui-components-skeleton.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-slider.mjs +347 -0
- package/fesm2022/raintonic-formaui-components-slider.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-spinner.mjs +63 -0
- package/fesm2022/raintonic-formaui-components-spinner.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-stepper.mjs +317 -0
- package/fesm2022/raintonic-formaui-components-stepper.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-tab.mjs +197 -0
- package/fesm2022/raintonic-formaui-components-tab.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-tag.mjs +78 -0
- package/fesm2022/raintonic-formaui-components-tag.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-time-picker.mjs +644 -0
- package/fesm2022/raintonic-formaui-components-time-picker.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-toggle.mjs +171 -0
- package/fesm2022/raintonic-formaui-components-toggle.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-toolbar.mjs +140 -0
- package/fesm2022/raintonic-formaui-components-toolbar.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-tooltip.mjs +555 -0
- package/fesm2022/raintonic-formaui-components-tooltip.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-tree-select.mjs +314 -0
- package/fesm2022/raintonic-formaui-components-tree-select.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-tree-table.mjs +103 -0
- package/fesm2022/raintonic-formaui-components-tree-table.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-components-tree.mjs +430 -0
- package/fesm2022/raintonic-formaui-components-tree.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-core.mjs +62 -0
- package/fesm2022/raintonic-formaui-core.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-services-dialog.mjs +798 -0
- package/fesm2022/raintonic-formaui-services-dialog.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-services-notification.mjs +391 -0
- package/fesm2022/raintonic-formaui-services-notification.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-services-theme.mjs +248 -0
- package/fesm2022/raintonic-formaui-services-theme.mjs.map +1 -0
- package/fesm2022/raintonic-formaui-test-utils.mjs +66 -0
- package/fesm2022/raintonic-formaui-test-utils.mjs.map +1 -0
- package/fesm2022/raintonic-formaui.mjs +15 -0
- package/fesm2022/raintonic-formaui.mjs.map +1 -0
- package/llms-full.txt +1627 -0
- package/llms.txt +60 -0
- package/package.json +251 -0
- package/styles/_fonts-entry.scss +3 -0
- package/styles/fonts/dm-mono-400-latin.woff2 +0 -0
- package/styles/fonts/inter-tight-latin-italic.woff2 +0 -0
- package/styles/fonts/inter-tight-latin.woff2 +0 -0
- package/styles/index.scss +127 -0
- package/styles/partials/_constants.scss +29 -0
- package/styles/partials/_fonts.scss +36 -0
- package/styles/partials/_grid.scss +171 -0
- package/styles/partials/_mixins.scss +145 -0
- package/styles/partials/_motion.scss +252 -0
- package/styles/partials/_theme.scss +275 -0
- package/styles/partials/_typography.scss +112 -0
- package/styles/partials/_utilities.scss +480 -0
- package/styles/partials/themes/_dark.scss +254 -0
- package/styles/partials/themes/_light.scss +254 -0
- package/types/raintonic-formaui-cdk-drag-drop.d.ts +196 -0
- package/types/raintonic-formaui-cdk-drag-drop.d.ts.map +1 -0
- package/types/raintonic-formaui-cdk-form-field.d.ts +62 -0
- package/types/raintonic-formaui-cdk-form-field.d.ts.map +1 -0
- package/types/raintonic-formaui-cdk-overlay.d.ts +843 -0
- package/types/raintonic-formaui-cdk-overlay.d.ts.map +1 -0
- package/types/raintonic-formaui-cdk-virtual-scroll.d.ts +112 -0
- package/types/raintonic-formaui-cdk-virtual-scroll.d.ts.map +1 -0
- package/types/raintonic-formaui-components-accordion.d.ts +124 -0
- package/types/raintonic-formaui-components-accordion.d.ts.map +1 -0
- package/types/raintonic-formaui-components-alert.d.ts +143 -0
- package/types/raintonic-formaui-components-alert.d.ts.map +1 -0
- package/types/raintonic-formaui-components-autocomplete.d.ts +193 -0
- package/types/raintonic-formaui-components-autocomplete.d.ts.map +1 -0
- package/types/raintonic-formaui-components-avatar.d.ts +52 -0
- package/types/raintonic-formaui-components-avatar.d.ts.map +1 -0
- package/types/raintonic-formaui-components-badge.d.ts +47 -0
- package/types/raintonic-formaui-components-badge.d.ts.map +1 -0
- package/types/raintonic-formaui-components-big-menu.d.ts +62 -0
- package/types/raintonic-formaui-components-big-menu.d.ts.map +1 -0
- package/types/raintonic-formaui-components-breadcrumb.d.ts +26 -0
- package/types/raintonic-formaui-components-breadcrumb.d.ts.map +1 -0
- package/types/raintonic-formaui-components-button-group.d.ts +61 -0
- package/types/raintonic-formaui-components-button-group.d.ts.map +1 -0
- package/types/raintonic-formaui-components-button.d.ts +116 -0
- package/types/raintonic-formaui-components-button.d.ts.map +1 -0
- package/types/raintonic-formaui-components-card.d.ts +191 -0
- package/types/raintonic-formaui-components-card.d.ts.map +1 -0
- package/types/raintonic-formaui-components-checkbox.d.ts +132 -0
- package/types/raintonic-formaui-components-checkbox.d.ts.map +1 -0
- package/types/raintonic-formaui-components-data-table.d.ts +368 -0
- package/types/raintonic-formaui-components-data-table.d.ts.map +1 -0
- package/types/raintonic-formaui-components-date-picker.d.ts +341 -0
- package/types/raintonic-formaui-components-date-picker.d.ts.map +1 -0
- package/types/raintonic-formaui-components-divider.d.ts +21 -0
- package/types/raintonic-formaui-components-divider.d.ts.map +1 -0
- package/types/raintonic-formaui-components-drawer.d.ts +48 -0
- package/types/raintonic-formaui-components-drawer.d.ts.map +1 -0
- package/types/raintonic-formaui-components-dynamic-form.d.ts +412 -0
- package/types/raintonic-formaui-components-dynamic-form.d.ts.map +1 -0
- package/types/raintonic-formaui-components-empty-state.d.ts +14 -0
- package/types/raintonic-formaui-components-empty-state.d.ts.map +1 -0
- package/types/raintonic-formaui-components-file-upload.d.ts +77 -0
- package/types/raintonic-formaui-components-file-upload.d.ts.map +1 -0
- package/types/raintonic-formaui-components-form-field.d.ts +271 -0
- package/types/raintonic-formaui-components-form-field.d.ts.map +1 -0
- package/types/raintonic-formaui-components-icon.d.ts +61 -0
- package/types/raintonic-formaui-components-icon.d.ts.map +1 -0
- package/types/raintonic-formaui-components-input.d.ts +149 -0
- package/types/raintonic-formaui-components-input.d.ts.map +1 -0
- package/types/raintonic-formaui-components-list.d.ts +48 -0
- package/types/raintonic-formaui-components-list.d.ts.map +1 -0
- package/types/raintonic-formaui-components-menu.d.ts +403 -0
- package/types/raintonic-formaui-components-menu.d.ts.map +1 -0
- package/types/raintonic-formaui-components-number-input.d.ts +127 -0
- package/types/raintonic-formaui-components-number-input.d.ts.map +1 -0
- package/types/raintonic-formaui-components-paginator.d.ts +37 -0
- package/types/raintonic-formaui-components-paginator.d.ts.map +1 -0
- package/types/raintonic-formaui-components-password-input.d.ts +111 -0
- package/types/raintonic-formaui-components-password-input.d.ts.map +1 -0
- package/types/raintonic-formaui-components-popover.d.ts +131 -0
- package/types/raintonic-formaui-components-popover.d.ts.map +1 -0
- package/types/raintonic-formaui-components-progressbar.d.ts +111 -0
- package/types/raintonic-formaui-components-progressbar.d.ts.map +1 -0
- package/types/raintonic-formaui-components-radio.d.ts +95 -0
- package/types/raintonic-formaui-components-radio.d.ts.map +1 -0
- package/types/raintonic-formaui-components-select.d.ts +307 -0
- package/types/raintonic-formaui-components-select.d.ts.map +1 -0
- package/types/raintonic-formaui-components-side-panel.d.ts +51 -0
- package/types/raintonic-formaui-components-side-panel.d.ts.map +1 -0
- package/types/raintonic-formaui-components-sidebar.d.ts +174 -0
- package/types/raintonic-formaui-components-sidebar.d.ts.map +1 -0
- package/types/raintonic-formaui-components-skeleton.d.ts +20 -0
- package/types/raintonic-formaui-components-skeleton.d.ts.map +1 -0
- package/types/raintonic-formaui-components-slider.d.ts +108 -0
- package/types/raintonic-formaui-components-slider.d.ts.map +1 -0
- package/types/raintonic-formaui-components-spinner.d.ts +42 -0
- package/types/raintonic-formaui-components-spinner.d.ts.map +1 -0
- package/types/raintonic-formaui-components-stepper.d.ts +126 -0
- package/types/raintonic-formaui-components-stepper.d.ts.map +1 -0
- package/types/raintonic-formaui-components-tab.d.ts +96 -0
- package/types/raintonic-formaui-components-tab.d.ts.map +1 -0
- package/types/raintonic-formaui-components-tag.d.ts +34 -0
- package/types/raintonic-formaui-components-tag.d.ts.map +1 -0
- package/types/raintonic-formaui-components-time-picker.d.ts +172 -0
- package/types/raintonic-formaui-components-time-picker.d.ts.map +1 -0
- package/types/raintonic-formaui-components-toggle.d.ts +70 -0
- package/types/raintonic-formaui-components-toggle.d.ts.map +1 -0
- package/types/raintonic-formaui-components-toolbar.d.ts +128 -0
- package/types/raintonic-formaui-components-toolbar.d.ts.map +1 -0
- package/types/raintonic-formaui-components-tooltip.d.ts +268 -0
- package/types/raintonic-formaui-components-tooltip.d.ts.map +1 -0
- package/types/raintonic-formaui-components-tree-select.d.ts +80 -0
- package/types/raintonic-formaui-components-tree-select.d.ts.map +1 -0
- package/types/raintonic-formaui-components-tree-table.d.ts +90 -0
- package/types/raintonic-formaui-components-tree-table.d.ts.map +1 -0
- package/types/raintonic-formaui-components-tree.d.ts +104 -0
- package/types/raintonic-formaui-components-tree.d.ts.map +1 -0
- package/types/raintonic-formaui-core.d.ts +115 -0
- package/types/raintonic-formaui-core.d.ts.map +1 -0
- package/types/raintonic-formaui-services-dialog.d.ts +451 -0
- package/types/raintonic-formaui-services-dialog.d.ts.map +1 -0
- package/types/raintonic-formaui-services-notification.d.ts +221 -0
- package/types/raintonic-formaui-services-notification.d.ts.map +1 -0
- package/types/raintonic-formaui-services-theme.d.ts +126 -0
- package/types/raintonic-formaui-services-theme.d.ts.map +1 -0
- package/types/raintonic-formaui-test-utils.d.ts +24 -0
- package/types/raintonic-formaui-test-utils.d.ts.map +1 -0
- package/types/raintonic-formaui.d.ts +4 -0
- package/types/raintonic-formaui.d.ts.map +1 -0
|
@@ -0,0 +1,1757 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, ComponentRef, TemplateRef, EmbeddedViewRef, inject, ElementRef, Component, RendererFactory2, EnvironmentInjector, createComponent, Injectable, DOCUMENT as DOCUMENT$1 } from '@angular/core';
|
|
3
|
+
import { DOCUMENT } from '@angular/common';
|
|
4
|
+
import { Subject } from 'rxjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Injection token for the overlay container element
|
|
8
|
+
*/
|
|
9
|
+
const FUI_OVERLAY_CONTAINER = new InjectionToken('FUI_OVERLAY_CONTAINER');
|
|
10
|
+
/**
|
|
11
|
+
* Injection token for the overlay scroll strategy
|
|
12
|
+
*/
|
|
13
|
+
const FUI_OVERLAY_SCROLL_STRATEGY = new InjectionToken('FUI_OVERLAY_SCROLL_STRATEGY');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* # FuiOverlayRefImpl
|
|
17
|
+
*
|
|
18
|
+
* Implementation of the FuiOverlayRef interface. This class manages the lifecycle
|
|
19
|
+
* of an individual overlay instance, including content attachment, positioning,
|
|
20
|
+
* and cleanup.
|
|
21
|
+
*
|
|
22
|
+
* ## Features
|
|
23
|
+
* - Content attachment and detachment
|
|
24
|
+
* - Event handling (backdrop clicks, keyboard events)
|
|
25
|
+
* - Position and size updates
|
|
26
|
+
* - CSS class management
|
|
27
|
+
* - Proper cleanup and disposal
|
|
28
|
+
*/
|
|
29
|
+
class FuiOverlayRefImpl {
|
|
30
|
+
_id;
|
|
31
|
+
_overlayElement;
|
|
32
|
+
_renderer;
|
|
33
|
+
_zIndex;
|
|
34
|
+
_backdropElement = null;
|
|
35
|
+
_attachedContent = null;
|
|
36
|
+
_config;
|
|
37
|
+
_hasAttached = false;
|
|
38
|
+
// Event subjects
|
|
39
|
+
_backdropClick = new Subject();
|
|
40
|
+
_keydownEvents = new Subject();
|
|
41
|
+
_attachments = new Subject();
|
|
42
|
+
_detachments = new Subject();
|
|
43
|
+
// Event listeners
|
|
44
|
+
_backdropClickListener;
|
|
45
|
+
_keydownListener;
|
|
46
|
+
constructor(overlayElement, config, renderer, id, zIndex = 1000) {
|
|
47
|
+
this._id = id ?? this._generateId();
|
|
48
|
+
this._overlayElement = overlayElement;
|
|
49
|
+
this._config = { ...config };
|
|
50
|
+
this._renderer = renderer;
|
|
51
|
+
this._zIndex = zIndex;
|
|
52
|
+
requestAnimationFrame(() => {
|
|
53
|
+
this._setupOverlayElement();
|
|
54
|
+
this._setupBackdrop();
|
|
55
|
+
this._setupEventListeners();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/** Unique identifier for this overlay */
|
|
59
|
+
get id() {
|
|
60
|
+
return this._id;
|
|
61
|
+
}
|
|
62
|
+
/** The overlay pane element */
|
|
63
|
+
get overlayElement() {
|
|
64
|
+
return this._overlayElement;
|
|
65
|
+
}
|
|
66
|
+
/** The backdrop element (if any) */
|
|
67
|
+
get backdropElement() {
|
|
68
|
+
return this._backdropElement;
|
|
69
|
+
}
|
|
70
|
+
/** Observable that emits when the backdrop is clicked */
|
|
71
|
+
get backdropClick() {
|
|
72
|
+
return this._backdropClick.asObservable();
|
|
73
|
+
}
|
|
74
|
+
/** Observable that emits when a key is pressed while the overlay has focus */
|
|
75
|
+
get keydownEvents() {
|
|
76
|
+
return this._keydownEvents.asObservable();
|
|
77
|
+
}
|
|
78
|
+
/** Observable that emits when the overlay is attached to the DOM */
|
|
79
|
+
get attachments() {
|
|
80
|
+
return this._attachments.asObservable();
|
|
81
|
+
}
|
|
82
|
+
/** Observable that emits when the overlay is detached from the DOM */
|
|
83
|
+
get detachments() {
|
|
84
|
+
return this._detachments.asObservable();
|
|
85
|
+
}
|
|
86
|
+
/** Whether the overlay is currently attached */
|
|
87
|
+
get hasAttached() {
|
|
88
|
+
return this._hasAttached;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Attaches content to the overlay
|
|
92
|
+
*/
|
|
93
|
+
attach(content, injector) {
|
|
94
|
+
if (this._hasAttached) {
|
|
95
|
+
throw new Error('Overlay already has content attached');
|
|
96
|
+
}
|
|
97
|
+
// Hide overlay initially to prevent flash at (0,0) before positioning
|
|
98
|
+
this._renderer.setStyle(this._overlayElement, 'opacity', '0');
|
|
99
|
+
let attachedContent = null;
|
|
100
|
+
if (content instanceof ComponentRef) {
|
|
101
|
+
// Component content
|
|
102
|
+
attachedContent = content;
|
|
103
|
+
this._overlayElement.appendChild(content.location.nativeElement);
|
|
104
|
+
}
|
|
105
|
+
else if (content instanceof TemplateRef) {
|
|
106
|
+
// Template content
|
|
107
|
+
const viewRef = content.createEmbeddedView({}, injector);
|
|
108
|
+
attachedContent = viewRef;
|
|
109
|
+
viewRef.rootNodes.forEach((node) => {
|
|
110
|
+
this._overlayElement.appendChild(node);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
else if (content instanceof HTMLElement) {
|
|
114
|
+
// DOM element content
|
|
115
|
+
this._overlayElement.appendChild(content);
|
|
116
|
+
}
|
|
117
|
+
else if (typeof content === 'string') {
|
|
118
|
+
// String content
|
|
119
|
+
this._renderer.setProperty(this._overlayElement, 'textContent', content);
|
|
120
|
+
}
|
|
121
|
+
this._attachedContent = attachedContent;
|
|
122
|
+
this._hasAttached = true;
|
|
123
|
+
this._attachments.next();
|
|
124
|
+
// Apply position strategy after DOM has settled
|
|
125
|
+
// Use single RAF to ensure layout is complete, then make visible
|
|
126
|
+
if (this._config.positionStrategy) {
|
|
127
|
+
requestAnimationFrame(() => {
|
|
128
|
+
if (this._config.positionStrategy && this._hasAttached) {
|
|
129
|
+
this._config.positionStrategy.apply();
|
|
130
|
+
// Make overlay visible after positioning
|
|
131
|
+
this._renderer.setStyle(this._overlayElement, 'opacity', '1');
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
// No position strategy, make visible immediately
|
|
137
|
+
this._renderer.setStyle(this._overlayElement, 'opacity', '1');
|
|
138
|
+
}
|
|
139
|
+
// Enable scroll strategy if available
|
|
140
|
+
if (this._config.scrollStrategy) {
|
|
141
|
+
this._config.scrollStrategy.enable();
|
|
142
|
+
}
|
|
143
|
+
return attachedContent;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Detaches the content from the overlay
|
|
147
|
+
*/
|
|
148
|
+
detach() {
|
|
149
|
+
if (!this._hasAttached) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
// Disable scroll strategy
|
|
153
|
+
if (this._config.scrollStrategy) {
|
|
154
|
+
this._config.scrollStrategy.disable();
|
|
155
|
+
}
|
|
156
|
+
// Clean up attached content
|
|
157
|
+
if (this._attachedContent) {
|
|
158
|
+
if (this._attachedContent instanceof ComponentRef) {
|
|
159
|
+
this._attachedContent.destroy();
|
|
160
|
+
}
|
|
161
|
+
else if (this._attachedContent instanceof EmbeddedViewRef) {
|
|
162
|
+
this._attachedContent.destroy();
|
|
163
|
+
}
|
|
164
|
+
this._attachedContent = null;
|
|
165
|
+
}
|
|
166
|
+
// Clear overlay element content
|
|
167
|
+
while (this._overlayElement.firstChild) {
|
|
168
|
+
this._overlayElement.removeChild(this._overlayElement.firstChild);
|
|
169
|
+
}
|
|
170
|
+
this._hasAttached = false;
|
|
171
|
+
this._detachments.next();
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Disposes the overlay and cleans up resources
|
|
175
|
+
*/
|
|
176
|
+
dispose() {
|
|
177
|
+
// Detach content first
|
|
178
|
+
this.detach();
|
|
179
|
+
// Dispose position strategy
|
|
180
|
+
if (this._config.positionStrategy) {
|
|
181
|
+
this._config.positionStrategy.dispose();
|
|
182
|
+
}
|
|
183
|
+
// Dispose scroll strategy
|
|
184
|
+
if (this._config.scrollStrategy) {
|
|
185
|
+
this._config.scrollStrategy.detach();
|
|
186
|
+
}
|
|
187
|
+
// Remove event listeners
|
|
188
|
+
this._removeEventListeners();
|
|
189
|
+
// Remove backdrop
|
|
190
|
+
if (this._backdropElement?.parentElement) {
|
|
191
|
+
this._backdropElement.parentElement.removeChild(this._backdropElement);
|
|
192
|
+
}
|
|
193
|
+
// Remove overlay element
|
|
194
|
+
if (this._overlayElement.parentElement) {
|
|
195
|
+
this._overlayElement.parentElement.removeChild(this._overlayElement);
|
|
196
|
+
}
|
|
197
|
+
// Complete subjects
|
|
198
|
+
this._backdropClick.complete();
|
|
199
|
+
this._keydownEvents.complete();
|
|
200
|
+
this._attachments.complete();
|
|
201
|
+
this._detachments.complete();
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Updates the position of the overlay
|
|
205
|
+
*/
|
|
206
|
+
updatePosition() {
|
|
207
|
+
if (this._config.positionStrategy) {
|
|
208
|
+
this._config.positionStrategy.apply();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Updates the size of the overlay
|
|
213
|
+
*/
|
|
214
|
+
updateSize(config) {
|
|
215
|
+
if (config) {
|
|
216
|
+
Object.assign(this._config, config);
|
|
217
|
+
}
|
|
218
|
+
this._applySizeConfig();
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Adds CSS classes to the overlay pane
|
|
222
|
+
*/
|
|
223
|
+
addPanelClass(classes) {
|
|
224
|
+
const classArray = Array.isArray(classes) ? classes : [classes];
|
|
225
|
+
classArray.forEach((className) => {
|
|
226
|
+
if (className) {
|
|
227
|
+
this._renderer.addClass(this._overlayElement, className);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Removes CSS classes from the overlay pane
|
|
233
|
+
*/
|
|
234
|
+
removePanelClass(classes) {
|
|
235
|
+
const classArray = Array.isArray(classes) ? classes : [classes];
|
|
236
|
+
classArray.forEach((className) => {
|
|
237
|
+
if (className) {
|
|
238
|
+
this._renderer.removeClass(this._overlayElement, className);
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Gets the current configuration of the overlay
|
|
244
|
+
*/
|
|
245
|
+
getConfig() {
|
|
246
|
+
return { ...this._config };
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Updates the configuration of the overlay
|
|
250
|
+
*/
|
|
251
|
+
updateConfig(config) {
|
|
252
|
+
const oldConfig = { ...this._config };
|
|
253
|
+
Object.assign(this._config, config);
|
|
254
|
+
// Update backdrop if needed
|
|
255
|
+
if (config.hasBackdrop !== undefined && config.hasBackdrop !== oldConfig.hasBackdrop) {
|
|
256
|
+
if (config.hasBackdrop) {
|
|
257
|
+
this._createBackdrop();
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
this._removeBackdrop();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Update backdrop classes
|
|
264
|
+
const backdrop = this._backdropElement;
|
|
265
|
+
if (config.backdropClass && backdrop) {
|
|
266
|
+
// Remove old classes
|
|
267
|
+
if (oldConfig.backdropClass) {
|
|
268
|
+
const oldClasses = Array.isArray(oldConfig.backdropClass) ? oldConfig.backdropClass : [oldConfig.backdropClass];
|
|
269
|
+
oldClasses.forEach((className) => {
|
|
270
|
+
if (className) {
|
|
271
|
+
this._renderer.removeClass(backdrop, className);
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
// Add new classes
|
|
276
|
+
const newClasses = Array.isArray(config.backdropClass) ? config.backdropClass : [config.backdropClass];
|
|
277
|
+
newClasses.forEach((className) => {
|
|
278
|
+
if (className) {
|
|
279
|
+
this._renderer.addClass(backdrop, className);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
// Update panel classes
|
|
284
|
+
if (config.panelClass) {
|
|
285
|
+
// Remove old classes
|
|
286
|
+
if (oldConfig.panelClass) {
|
|
287
|
+
this.removePanelClass(oldConfig.panelClass);
|
|
288
|
+
}
|
|
289
|
+
// Add new classes
|
|
290
|
+
this.addPanelClass(config.panelClass);
|
|
291
|
+
}
|
|
292
|
+
// Update size
|
|
293
|
+
this._applySizeConfig();
|
|
294
|
+
// Update position strategy
|
|
295
|
+
if (config.positionStrategy && config.positionStrategy !== oldConfig.positionStrategy) {
|
|
296
|
+
if (oldConfig.positionStrategy) {
|
|
297
|
+
oldConfig.positionStrategy.dispose();
|
|
298
|
+
}
|
|
299
|
+
config.positionStrategy.setOverlayElement(this._overlayElement);
|
|
300
|
+
}
|
|
301
|
+
// Update scroll strategy
|
|
302
|
+
if (config.scrollStrategy && config.scrollStrategy !== oldConfig.scrollStrategy) {
|
|
303
|
+
if (oldConfig.scrollStrategy) {
|
|
304
|
+
oldConfig.scrollStrategy.detach();
|
|
305
|
+
}
|
|
306
|
+
config.scrollStrategy.attach(this);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
_setupOverlayElement() {
|
|
310
|
+
// Add base classes
|
|
311
|
+
this._renderer.addClass(this._overlayElement, 'fui-overlay-pane');
|
|
312
|
+
// Add custom panel classes
|
|
313
|
+
if (this._config.panelClass) {
|
|
314
|
+
this.addPanelClass(this._config.panelClass);
|
|
315
|
+
}
|
|
316
|
+
// Apply z-index for proper stacking of nested overlays
|
|
317
|
+
this._renderer.setStyle(this._overlayElement, 'z-index', String(this._zIndex));
|
|
318
|
+
// Apply size configuration
|
|
319
|
+
this._applySizeConfig();
|
|
320
|
+
// Set direction
|
|
321
|
+
if (this._config.direction) {
|
|
322
|
+
this._renderer.setAttribute(this._overlayElement, 'dir', this._config.direction);
|
|
323
|
+
}
|
|
324
|
+
// Set position strategy
|
|
325
|
+
if (this._config.positionStrategy) {
|
|
326
|
+
this._config.positionStrategy.setOverlayElement(this._overlayElement);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
_setupBackdrop() {
|
|
330
|
+
if (this._config.hasBackdrop) {
|
|
331
|
+
this._createBackdrop();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
_createBackdrop() {
|
|
335
|
+
if (this._backdropElement) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
this._backdropElement = this._renderer.createElement('div');
|
|
339
|
+
this._renderer.addClass(this._backdropElement, 'fui-overlay-backdrop');
|
|
340
|
+
// Apply z-index for backdrop (slightly below the overlay panel)
|
|
341
|
+
this._renderer.setStyle(this._backdropElement, 'z-index', String(this._zIndex - 1));
|
|
342
|
+
// Add custom backdrop classes
|
|
343
|
+
const backdropEl = this._backdropElement;
|
|
344
|
+
if (this._config.backdropClass && backdropEl) {
|
|
345
|
+
const classes = Array.isArray(this._config.backdropClass)
|
|
346
|
+
? this._config.backdropClass
|
|
347
|
+
: [this._config.backdropClass];
|
|
348
|
+
classes.forEach((className) => {
|
|
349
|
+
if (className) {
|
|
350
|
+
this._renderer.addClass(backdropEl, className);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
// Insert backdrop before overlay element
|
|
355
|
+
const parent = this._overlayElement.parentElement;
|
|
356
|
+
if (parent) {
|
|
357
|
+
this._renderer.insertBefore(parent, this._backdropElement, this._overlayElement);
|
|
358
|
+
}
|
|
359
|
+
// Setup backdrop click listener
|
|
360
|
+
this._backdropClickListener = (event) => {
|
|
361
|
+
if (event.target === this._backdropElement) {
|
|
362
|
+
this._backdropClick.next(event);
|
|
363
|
+
}
|
|
364
|
+
};
|
|
365
|
+
if (this._backdropElement) {
|
|
366
|
+
this._backdropElement.addEventListener('click', this._backdropClickListener);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
_removeBackdrop() {
|
|
370
|
+
if (this._backdropElement) {
|
|
371
|
+
if (this._backdropClickListener) {
|
|
372
|
+
this._backdropElement.removeEventListener('click', this._backdropClickListener);
|
|
373
|
+
this._backdropClickListener = undefined;
|
|
374
|
+
}
|
|
375
|
+
if (this._backdropElement.parentElement) {
|
|
376
|
+
this._backdropElement.parentElement.removeChild(this._backdropElement);
|
|
377
|
+
}
|
|
378
|
+
this._backdropElement = null;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
_setupEventListeners() {
|
|
382
|
+
// Keydown events
|
|
383
|
+
this._keydownListener = (event) => {
|
|
384
|
+
this._keydownEvents.next(event);
|
|
385
|
+
};
|
|
386
|
+
this._overlayElement.addEventListener('keydown', this._keydownListener);
|
|
387
|
+
}
|
|
388
|
+
_removeEventListeners() {
|
|
389
|
+
if (this._keydownListener) {
|
|
390
|
+
this._overlayElement.removeEventListener('keydown', this._keydownListener);
|
|
391
|
+
this._keydownListener = undefined;
|
|
392
|
+
}
|
|
393
|
+
if (this._backdropClickListener && this._backdropElement) {
|
|
394
|
+
this._backdropElement.removeEventListener('click', this._backdropClickListener);
|
|
395
|
+
this._backdropClickListener = undefined;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
_applySizeConfig() {
|
|
399
|
+
const config = this._config;
|
|
400
|
+
if (config.width !== undefined) {
|
|
401
|
+
this._renderer.setStyle(this._overlayElement, 'width', this._coerceCssPixelValue(config.width));
|
|
402
|
+
}
|
|
403
|
+
if (config.height !== undefined) {
|
|
404
|
+
this._renderer.setStyle(this._overlayElement, 'height', this._coerceCssPixelValue(config.height));
|
|
405
|
+
}
|
|
406
|
+
if (config.minWidth !== undefined) {
|
|
407
|
+
this._renderer.setStyle(this._overlayElement, 'min-width', this._coerceCssPixelValue(config.minWidth));
|
|
408
|
+
}
|
|
409
|
+
if (config.minHeight !== undefined) {
|
|
410
|
+
this._renderer.setStyle(this._overlayElement, 'min-height', this._coerceCssPixelValue(config.minHeight));
|
|
411
|
+
}
|
|
412
|
+
if (config.maxWidth !== undefined) {
|
|
413
|
+
this._renderer.setStyle(this._overlayElement, 'max-width', this._coerceCssPixelValue(config.maxWidth));
|
|
414
|
+
}
|
|
415
|
+
if (config.maxHeight !== undefined) {
|
|
416
|
+
this._renderer.setStyle(this._overlayElement, 'max-height', this._coerceCssPixelValue(config.maxHeight));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
_coerceCssPixelValue(value) {
|
|
420
|
+
return typeof value === 'number' ? `${value}px` : value;
|
|
421
|
+
}
|
|
422
|
+
_generateId() {
|
|
423
|
+
return `fui-overlay-${Math.random().toString(36).substr(2, 9)}`;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* # FuiOverlayContainer Component
|
|
429
|
+
*
|
|
430
|
+
* Container component that holds all overlay content. This component is automatically
|
|
431
|
+
* created and attached to the document body when the overlay service is first used.
|
|
432
|
+
*
|
|
433
|
+
* ## Features
|
|
434
|
+
* - Provides a dedicated container for all overlay content
|
|
435
|
+
* - Manages z-index stacking for multiple overlays
|
|
436
|
+
* - Handles cleanup when destroyed
|
|
437
|
+
* - Applies proper CSS classes for styling
|
|
438
|
+
*
|
|
439
|
+
* ## Usage
|
|
440
|
+
*
|
|
441
|
+
* This component is used internally by the overlay service and should not be
|
|
442
|
+
* instantiated directly by application code.
|
|
443
|
+
*/
|
|
444
|
+
class FuiOverlayContainerComponent {
|
|
445
|
+
_document = inject(DOCUMENT);
|
|
446
|
+
_elementRef = inject((ElementRef));
|
|
447
|
+
ngOnInit() {
|
|
448
|
+
// Ensure the container is attached to the document body
|
|
449
|
+
if (this._elementRef.nativeElement.parentElement !== this._document.body) {
|
|
450
|
+
this._document.body.appendChild(this._elementRef.nativeElement);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
ngOnDestroy() {
|
|
454
|
+
// Clean up the container element
|
|
455
|
+
const element = this._elementRef.nativeElement;
|
|
456
|
+
if (element.parentElement) {
|
|
457
|
+
element.parentElement.removeChild(element);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Gets the container element
|
|
462
|
+
*/
|
|
463
|
+
getContainerElement() {
|
|
464
|
+
return this._elementRef.nativeElement;
|
|
465
|
+
}
|
|
466
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: FuiOverlayContainerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
467
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.2.6", type: FuiOverlayContainerComponent, isStandalone: true, selector: "fui-overlay-container", host: { properties: { "attr.aria-live": "\"polite\"", "attr.aria-atomic": "\"true\"" }, classAttribute: "fui-overlay-container" }, ngImport: i0, template: '', isInline: true, styles: [":host{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:var(--fui-z-popover)}\n"] });
|
|
468
|
+
}
|
|
469
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: FuiOverlayContainerComponent, decorators: [{
|
|
470
|
+
type: Component,
|
|
471
|
+
args: [{ selector: 'fui-overlay-container', standalone: true, template: '', host: {
|
|
472
|
+
class: 'fui-overlay-container',
|
|
473
|
+
'[attr.aria-live]': '"polite"',
|
|
474
|
+
'[attr.aria-atomic]': '"true"',
|
|
475
|
+
}, styles: [":host{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:var(--fui-z-popover)}\n"] }]
|
|
476
|
+
}] });
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* # FuiGlobalPositionStrategy
|
|
480
|
+
*
|
|
481
|
+
* A position strategy that positions the overlay at a fixed position on the screen.
|
|
482
|
+
* This strategy is useful for modals, dialogs, and other overlays that should be
|
|
483
|
+
* positioned relative to the viewport rather than a specific element.
|
|
484
|
+
*
|
|
485
|
+
* ## Features
|
|
486
|
+
* - Fixed positioning relative to the viewport
|
|
487
|
+
* - Support for all CSS position properties (top, bottom, left, right)
|
|
488
|
+
* - Centering options (horizontal and vertical)
|
|
489
|
+
* - Responsive positioning
|
|
490
|
+
* - Size constraints (width, height, min/max dimensions)
|
|
491
|
+
*/
|
|
492
|
+
class FuiGlobalPositionStrategy {
|
|
493
|
+
_renderer;
|
|
494
|
+
_options;
|
|
495
|
+
_overlayElement;
|
|
496
|
+
_positionChanges = new Subject();
|
|
497
|
+
_isDisposed = false;
|
|
498
|
+
constructor(_renderer, _options = {}) {
|
|
499
|
+
this._renderer = _renderer;
|
|
500
|
+
this._options = _options;
|
|
501
|
+
}
|
|
502
|
+
/** Observable that emits when the position changes */
|
|
503
|
+
get positionChanges() {
|
|
504
|
+
return this._positionChanges.asObservable();
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Sets the overlay element reference
|
|
508
|
+
*/
|
|
509
|
+
setOverlayElement(element) {
|
|
510
|
+
this._overlayElement = element;
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Applies the positioning strategy
|
|
514
|
+
*/
|
|
515
|
+
apply() {
|
|
516
|
+
if (!this._overlayElement || this._isDisposed) {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
// Set position to fixed for global positioning
|
|
520
|
+
this._renderer.setStyle(this._overlayElement, 'position', 'fixed');
|
|
521
|
+
// Apply positioning
|
|
522
|
+
this._applyPosition();
|
|
523
|
+
// Apply centering if requested
|
|
524
|
+
this._applyCentering();
|
|
525
|
+
// Apply size constraints
|
|
526
|
+
this._applySizeConstraints();
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Sets the top position
|
|
530
|
+
*/
|
|
531
|
+
top(value = '0') {
|
|
532
|
+
this._options.top = value;
|
|
533
|
+
this._options.bottom = undefined;
|
|
534
|
+
return this;
|
|
535
|
+
}
|
|
536
|
+
/**
|
|
537
|
+
* Sets the bottom position
|
|
538
|
+
*/
|
|
539
|
+
bottom(value = '0') {
|
|
540
|
+
this._options.bottom = value;
|
|
541
|
+
this._options.top = undefined;
|
|
542
|
+
return this;
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* Sets the left position
|
|
546
|
+
*/
|
|
547
|
+
left(value = '0') {
|
|
548
|
+
this._options.left = value;
|
|
549
|
+
this._options.right = undefined;
|
|
550
|
+
return this;
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Sets the right position
|
|
554
|
+
*/
|
|
555
|
+
right(value = '0') {
|
|
556
|
+
this._options.right = value;
|
|
557
|
+
this._options.left = undefined;
|
|
558
|
+
return this;
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* Centers the overlay horizontally
|
|
562
|
+
*/
|
|
563
|
+
centerHorizontally() {
|
|
564
|
+
this._options.centerHorizontally = true;
|
|
565
|
+
this._options.left = undefined;
|
|
566
|
+
this._options.right = undefined;
|
|
567
|
+
return this;
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Centers the overlay vertically
|
|
571
|
+
*/
|
|
572
|
+
centerVertically() {
|
|
573
|
+
this._options.centerVertically = true;
|
|
574
|
+
this._options.top = undefined;
|
|
575
|
+
this._options.bottom = undefined;
|
|
576
|
+
return this;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Sets the width of the overlay
|
|
580
|
+
*/
|
|
581
|
+
width(value) {
|
|
582
|
+
this._options.width = value;
|
|
583
|
+
return this;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Sets the height of the overlay
|
|
587
|
+
*/
|
|
588
|
+
height(value) {
|
|
589
|
+
this._options.height = value;
|
|
590
|
+
return this;
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Sets the minimum width of the overlay
|
|
594
|
+
*/
|
|
595
|
+
minWidth(value) {
|
|
596
|
+
this._options.minWidth = value;
|
|
597
|
+
return this;
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Sets the minimum height of the overlay
|
|
601
|
+
*/
|
|
602
|
+
minHeight(value) {
|
|
603
|
+
this._options.minHeight = value;
|
|
604
|
+
return this;
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
607
|
+
* Sets the maximum width of the overlay
|
|
608
|
+
*/
|
|
609
|
+
maxWidth(value) {
|
|
610
|
+
this._options.maxWidth = value;
|
|
611
|
+
return this;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Sets the maximum height of the overlay
|
|
615
|
+
*/
|
|
616
|
+
maxHeight(value) {
|
|
617
|
+
this._options.maxHeight = value;
|
|
618
|
+
return this;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Disposes the strategy and cleans up resources
|
|
622
|
+
*/
|
|
623
|
+
dispose() {
|
|
624
|
+
if (this._isDisposed) {
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
this._isDisposed = true;
|
|
628
|
+
this._positionChanges.complete();
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Detaches the strategy from the overlay
|
|
632
|
+
*/
|
|
633
|
+
detach() {
|
|
634
|
+
// Global position strategy doesn't need special detach logic
|
|
635
|
+
}
|
|
636
|
+
_applyPosition() {
|
|
637
|
+
if (!this._overlayElement) {
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const options = this._options;
|
|
641
|
+
// Apply top/bottom positioning
|
|
642
|
+
if (options.top !== undefined) {
|
|
643
|
+
this._renderer.setStyle(this._overlayElement, 'top', this._coerceCssPixelValue(options.top));
|
|
644
|
+
this._renderer.removeStyle(this._overlayElement, 'bottom');
|
|
645
|
+
}
|
|
646
|
+
else if (options.bottom !== undefined) {
|
|
647
|
+
this._renderer.setStyle(this._overlayElement, 'bottom', this._coerceCssPixelValue(options.bottom));
|
|
648
|
+
this._renderer.removeStyle(this._overlayElement, 'top');
|
|
649
|
+
}
|
|
650
|
+
// Apply left/right positioning
|
|
651
|
+
if (options.left !== undefined) {
|
|
652
|
+
this._renderer.setStyle(this._overlayElement, 'left', this._coerceCssPixelValue(options.left));
|
|
653
|
+
this._renderer.removeStyle(this._overlayElement, 'right');
|
|
654
|
+
}
|
|
655
|
+
else if (options.right !== undefined) {
|
|
656
|
+
this._renderer.setStyle(this._overlayElement, 'right', this._coerceCssPixelValue(options.right));
|
|
657
|
+
this._renderer.removeStyle(this._overlayElement, 'left');
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
_applyCentering() {
|
|
661
|
+
if (!this._overlayElement) {
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
const options = this._options;
|
|
665
|
+
if (options.centerHorizontally) {
|
|
666
|
+
this._renderer.setStyle(this._overlayElement, 'left', '50%');
|
|
667
|
+
this._renderer.setStyle(this._overlayElement, 'transform', 'translateX(-50%)');
|
|
668
|
+
this._renderer.removeStyle(this._overlayElement, 'right');
|
|
669
|
+
}
|
|
670
|
+
if (options.centerVertically) {
|
|
671
|
+
this._renderer.setStyle(this._overlayElement, 'top', '50%');
|
|
672
|
+
const existingTransform = this._overlayElement.style.transform;
|
|
673
|
+
if (existingTransform?.includes('translateX')) {
|
|
674
|
+
this._renderer.setStyle(this._overlayElement, 'transform', 'translate(-50%, -50%)');
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
this._renderer.setStyle(this._overlayElement, 'transform', 'translateY(-50%)');
|
|
678
|
+
}
|
|
679
|
+
this._renderer.removeStyle(this._overlayElement, 'bottom');
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
_applySizeConstraints() {
|
|
683
|
+
if (!this._overlayElement) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
const options = this._options;
|
|
687
|
+
if (options.width !== undefined) {
|
|
688
|
+
this._renderer.setStyle(this._overlayElement, 'width', this._coerceCssPixelValue(options.width));
|
|
689
|
+
}
|
|
690
|
+
if (options.height !== undefined) {
|
|
691
|
+
this._renderer.setStyle(this._overlayElement, 'height', this._coerceCssPixelValue(options.height));
|
|
692
|
+
}
|
|
693
|
+
if (options.minWidth !== undefined) {
|
|
694
|
+
this._renderer.setStyle(this._overlayElement, 'min-width', this._coerceCssPixelValue(options.minWidth));
|
|
695
|
+
}
|
|
696
|
+
if (options.minHeight !== undefined) {
|
|
697
|
+
this._renderer.setStyle(this._overlayElement, 'min-height', this._coerceCssPixelValue(options.minHeight));
|
|
698
|
+
}
|
|
699
|
+
if (options.maxWidth !== undefined) {
|
|
700
|
+
this._renderer.setStyle(this._overlayElement, 'max-width', this._coerceCssPixelValue(options.maxWidth));
|
|
701
|
+
}
|
|
702
|
+
if (options.maxHeight !== undefined) {
|
|
703
|
+
this._renderer.setStyle(this._overlayElement, 'max-height', this._coerceCssPixelValue(options.maxHeight));
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
_coerceCssPixelValue(value) {
|
|
707
|
+
return typeof value === 'number' ? `${value}px` : value;
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* # FuiConnectedPositionStrategy
|
|
713
|
+
*
|
|
714
|
+
* A position strategy that positions the overlay relative to a connected element.
|
|
715
|
+
* This strategy is ideal for tooltips, dropdowns, menus, and other overlays that
|
|
716
|
+
* should be positioned relative to a trigger element.
|
|
717
|
+
*
|
|
718
|
+
* ## Features
|
|
719
|
+
* - Positions overlay relative to an origin element
|
|
720
|
+
* - Multiple fallback positions with automatic selection
|
|
721
|
+
* - Collision detection and viewport boundary handling
|
|
722
|
+
* - Push and grow behaviors for constrained spaces
|
|
723
|
+
* - Scroll and resize event handling
|
|
724
|
+
* - Transform origin support for animations
|
|
725
|
+
*/
|
|
726
|
+
class FuiConnectedPositionStrategy {
|
|
727
|
+
_renderer;
|
|
728
|
+
_overlayElement;
|
|
729
|
+
_originElement;
|
|
730
|
+
_positions;
|
|
731
|
+
_appliedPosition;
|
|
732
|
+
_isDisposed = false;
|
|
733
|
+
_resizeSubscription;
|
|
734
|
+
_scrollSubscription;
|
|
735
|
+
_positionChanges = new Subject();
|
|
736
|
+
// Configuration options
|
|
737
|
+
_withPush = true;
|
|
738
|
+
_withGrowAfterOpen = false;
|
|
739
|
+
_withLockedPosition = false;
|
|
740
|
+
_defaultOffsetX = 0;
|
|
741
|
+
_defaultOffsetY = 0;
|
|
742
|
+
_transformOriginElement;
|
|
743
|
+
_viewportMargin = 0;
|
|
744
|
+
_withFlexibleDimensions = false;
|
|
745
|
+
constructor(_renderer, options) {
|
|
746
|
+
this._renderer = _renderer;
|
|
747
|
+
this._originElement = options.origin instanceof ElementRef ? options.origin.nativeElement : options.origin;
|
|
748
|
+
this._positions = [...options.positions];
|
|
749
|
+
// Apply configuration options
|
|
750
|
+
if (options.withPush !== undefined) {
|
|
751
|
+
this._withPush = options.withPush;
|
|
752
|
+
}
|
|
753
|
+
if (options.withGrowAfterOpen !== undefined) {
|
|
754
|
+
this._withGrowAfterOpen = options.withGrowAfterOpen;
|
|
755
|
+
}
|
|
756
|
+
if (options.withLockedPosition !== undefined) {
|
|
757
|
+
this._withLockedPosition = options.withLockedPosition;
|
|
758
|
+
}
|
|
759
|
+
if (options.withDefaultOffsetX !== undefined) {
|
|
760
|
+
this._defaultOffsetX = options.withDefaultOffsetX;
|
|
761
|
+
}
|
|
762
|
+
if (options.withDefaultOffsetY !== undefined) {
|
|
763
|
+
this._defaultOffsetY = options.withDefaultOffsetY;
|
|
764
|
+
}
|
|
765
|
+
if (options.withTransformOriginOn) {
|
|
766
|
+
this._transformOriginElement =
|
|
767
|
+
options.withTransformOriginOn instanceof ElementRef
|
|
768
|
+
? options.withTransformOriginOn.nativeElement
|
|
769
|
+
: options.withTransformOriginOn;
|
|
770
|
+
}
|
|
771
|
+
if (options.viewportMargin !== undefined) {
|
|
772
|
+
this._viewportMargin = options.viewportMargin;
|
|
773
|
+
}
|
|
774
|
+
if (options.withFlexibleDimensions !== undefined) {
|
|
775
|
+
this._withFlexibleDimensions = options.withFlexibleDimensions;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
/** Observable that emits when the position changes */
|
|
779
|
+
get positionChanges() {
|
|
780
|
+
return this._positionChanges.asObservable();
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* Sets the overlay element reference
|
|
784
|
+
*/
|
|
785
|
+
setOverlayElement(element) {
|
|
786
|
+
this._overlayElement = element;
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Applies the positioning strategy
|
|
790
|
+
*/
|
|
791
|
+
apply() {
|
|
792
|
+
if (!this._overlayElement || this._isDisposed) {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
// Set position to fixed for precise positioning
|
|
796
|
+
this._renderer.setStyle(this._overlayElement, 'position', 'fixed');
|
|
797
|
+
// Force a layout recalculation to ensure accurate dimensions
|
|
798
|
+
// This is necessary to get the correct bounding rect
|
|
799
|
+
void this._overlayElement.offsetHeight;
|
|
800
|
+
// Find the best position
|
|
801
|
+
const bestPosition = this._getBestPosition();
|
|
802
|
+
if (bestPosition) {
|
|
803
|
+
this._applyPosition(bestPosition);
|
|
804
|
+
this._appliedPosition = bestPosition;
|
|
805
|
+
// Set up event listeners for repositioning
|
|
806
|
+
this._setupEventListeners();
|
|
807
|
+
// Emit position change
|
|
808
|
+
this._positionChanges.next({
|
|
809
|
+
connectionPair: bestPosition,
|
|
810
|
+
scrollableViewProperties: this._getScrollableViewProperties(),
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Updates the positions array
|
|
816
|
+
*/
|
|
817
|
+
withPositions(positions) {
|
|
818
|
+
this._positions = [...positions];
|
|
819
|
+
return this;
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Enables pushing the overlay on-screen if it would otherwise be clipped
|
|
823
|
+
*/
|
|
824
|
+
withPush(enabled = true) {
|
|
825
|
+
this._withPush = enabled;
|
|
826
|
+
return this;
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Enables growing the overlay after it reaches the edge of the viewport
|
|
830
|
+
*/
|
|
831
|
+
withGrowAfterOpen(enabled = true) {
|
|
832
|
+
this._withGrowAfterOpen = enabled;
|
|
833
|
+
return this;
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Locks the position after the first time it is positioned
|
|
837
|
+
*/
|
|
838
|
+
withLockedPosition(enabled = true) {
|
|
839
|
+
this._withLockedPosition = enabled;
|
|
840
|
+
return this;
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Sets the default offset for the x-axis
|
|
844
|
+
*/
|
|
845
|
+
withDefaultOffsetX(offset) {
|
|
846
|
+
this._defaultOffsetX = offset;
|
|
847
|
+
return this;
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Sets the default offset for the y-axis
|
|
851
|
+
*/
|
|
852
|
+
withDefaultOffsetY(offset) {
|
|
853
|
+
this._defaultOffsetY = offset;
|
|
854
|
+
return this;
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Sets the element to use for transform origin
|
|
858
|
+
*/
|
|
859
|
+
withTransformOriginOn(element) {
|
|
860
|
+
this._transformOriginElement = element instanceof ElementRef ? element.nativeElement : element;
|
|
861
|
+
return this;
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Sets the margin between the overlay and the viewport edge
|
|
865
|
+
*/
|
|
866
|
+
withViewportMargin(margin) {
|
|
867
|
+
this._viewportMargin = margin;
|
|
868
|
+
return this;
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* Enables flexible dimensions
|
|
872
|
+
*/
|
|
873
|
+
withFlexibleDimensions(enabled = true) {
|
|
874
|
+
this._withFlexibleDimensions = enabled;
|
|
875
|
+
return this;
|
|
876
|
+
}
|
|
877
|
+
/**
|
|
878
|
+
* Disposes the strategy and cleans up resources
|
|
879
|
+
*/
|
|
880
|
+
dispose() {
|
|
881
|
+
if (this._isDisposed) {
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
this._isDisposed = true;
|
|
885
|
+
this._removeEventListeners();
|
|
886
|
+
this._positionChanges.complete();
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Detaches the strategy from the overlay
|
|
890
|
+
*/
|
|
891
|
+
detach() {
|
|
892
|
+
this._removeEventListeners();
|
|
893
|
+
}
|
|
894
|
+
_getBestPosition() {
|
|
895
|
+
if (!this._overlayElement || !this._originElement) {
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
// If position is locked and we have an applied position, use it
|
|
899
|
+
if (this._withLockedPosition && this._appliedPosition) {
|
|
900
|
+
return this._appliedPosition;
|
|
901
|
+
}
|
|
902
|
+
// Force layout recalculation to ensure overlay has correct dimensions
|
|
903
|
+
// This is critical for center-aligned positions (right, left, top, bottom)
|
|
904
|
+
void this._overlayElement.offsetHeight;
|
|
905
|
+
void this._overlayElement.offsetWidth;
|
|
906
|
+
const originRect = this._originElement.getBoundingClientRect();
|
|
907
|
+
const overlayRect = this._overlayElement.getBoundingClientRect();
|
|
908
|
+
const viewportSize = this._getViewportSize();
|
|
909
|
+
let bestPosition = null;
|
|
910
|
+
let bestScore = -1;
|
|
911
|
+
for (const position of this._positions) {
|
|
912
|
+
const positionedRect = this._calculatePositionedRect(position, originRect, overlayRect);
|
|
913
|
+
const score = this._calculatePositionScore(positionedRect, viewportSize);
|
|
914
|
+
if (score > bestScore) {
|
|
915
|
+
bestScore = score;
|
|
916
|
+
bestPosition = position;
|
|
917
|
+
}
|
|
918
|
+
// If we found a perfect position (fully visible), use it
|
|
919
|
+
if (score === 1) {
|
|
920
|
+
break;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return bestPosition;
|
|
924
|
+
}
|
|
925
|
+
_applyPosition(position) {
|
|
926
|
+
if (!this._overlayElement || !this._originElement) {
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
// Force layout recalculation again to ensure dimensions are current
|
|
930
|
+
void this._overlayElement.offsetHeight;
|
|
931
|
+
void this._overlayElement.offsetWidth;
|
|
932
|
+
const originRect = this._originElement.getBoundingClientRect();
|
|
933
|
+
const overlayRect = this._overlayElement.getBoundingClientRect();
|
|
934
|
+
const positionedRect = this._calculatePositionedRect(position, originRect, overlayRect);
|
|
935
|
+
let { x, y } = positionedRect;
|
|
936
|
+
// Apply push behavior if enabled
|
|
937
|
+
if (this._withPush) {
|
|
938
|
+
const pushedPosition = this._pushWithinViewport({ x, y, width: overlayRect.width, height: overlayRect.height });
|
|
939
|
+
x = pushedPosition.x;
|
|
940
|
+
y = pushedPosition.y;
|
|
941
|
+
}
|
|
942
|
+
// Apply the position
|
|
943
|
+
this._renderer.setStyle(this._overlayElement, 'left', `${x}px`);
|
|
944
|
+
this._renderer.setStyle(this._overlayElement, 'top', `${y}px`);
|
|
945
|
+
// Apply transform origin if specified
|
|
946
|
+
if (this._transformOriginElement) {
|
|
947
|
+
this._setTransformOrigin(position);
|
|
948
|
+
}
|
|
949
|
+
// Apply flexible dimensions if enabled
|
|
950
|
+
if (this._withFlexibleDimensions) {
|
|
951
|
+
this._applyFlexibleDimensions(position, { x, y, width: overlayRect.width, height: overlayRect.height });
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
_calculatePositionedRect(position, originRect, overlayRect) {
|
|
955
|
+
const offsetX = (position.offsetX ?? 0) + this._defaultOffsetX;
|
|
956
|
+
const offsetY = (position.offsetY ?? 0) + this._defaultOffsetY;
|
|
957
|
+
let x;
|
|
958
|
+
let y;
|
|
959
|
+
// Calculate x position
|
|
960
|
+
switch (position.originX) {
|
|
961
|
+
case 'start':
|
|
962
|
+
x = originRect.left;
|
|
963
|
+
break;
|
|
964
|
+
case 'center':
|
|
965
|
+
x = originRect.left + originRect.width / 2;
|
|
966
|
+
break;
|
|
967
|
+
case 'end':
|
|
968
|
+
x = originRect.right;
|
|
969
|
+
break;
|
|
970
|
+
}
|
|
971
|
+
switch (position.overlayX) {
|
|
972
|
+
case 'start':
|
|
973
|
+
// x remains as is
|
|
974
|
+
break;
|
|
975
|
+
case 'center':
|
|
976
|
+
x -= overlayRect.width / 2;
|
|
977
|
+
break;
|
|
978
|
+
case 'end':
|
|
979
|
+
x -= overlayRect.width;
|
|
980
|
+
break;
|
|
981
|
+
}
|
|
982
|
+
// Calculate y position
|
|
983
|
+
switch (position.originY) {
|
|
984
|
+
case 'top':
|
|
985
|
+
y = originRect.top;
|
|
986
|
+
break;
|
|
987
|
+
case 'center':
|
|
988
|
+
y = originRect.top + originRect.height / 2;
|
|
989
|
+
break;
|
|
990
|
+
case 'bottom':
|
|
991
|
+
y = originRect.bottom;
|
|
992
|
+
break;
|
|
993
|
+
}
|
|
994
|
+
switch (position.overlayY) {
|
|
995
|
+
case 'top':
|
|
996
|
+
// y remains as is
|
|
997
|
+
break;
|
|
998
|
+
case 'center':
|
|
999
|
+
y -= overlayRect.height / 2;
|
|
1000
|
+
break;
|
|
1001
|
+
case 'bottom':
|
|
1002
|
+
y -= overlayRect.height;
|
|
1003
|
+
break;
|
|
1004
|
+
}
|
|
1005
|
+
return {
|
|
1006
|
+
x: x + offsetX,
|
|
1007
|
+
y: y + offsetY,
|
|
1008
|
+
width: overlayRect.width,
|
|
1009
|
+
height: overlayRect.height,
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
_calculatePositionScore(rect, viewportSize) {
|
|
1013
|
+
const margin = this._viewportMargin;
|
|
1014
|
+
const visibleArea = Math.max(0, Math.min(rect.x + rect.width, viewportSize.width - margin) - Math.max(rect.x, margin)) *
|
|
1015
|
+
Math.max(0, Math.min(rect.y + rect.height, viewportSize.height - margin) - Math.max(rect.y, margin));
|
|
1016
|
+
const totalArea = rect.width * rect.height;
|
|
1017
|
+
return totalArea > 0 ? visibleArea / totalArea : 0;
|
|
1018
|
+
}
|
|
1019
|
+
_pushWithinViewport(rect) {
|
|
1020
|
+
const viewportSize = this._getViewportSize();
|
|
1021
|
+
const margin = this._viewportMargin;
|
|
1022
|
+
let { x, y } = rect;
|
|
1023
|
+
// Push horizontally
|
|
1024
|
+
if (x < margin) {
|
|
1025
|
+
x = margin;
|
|
1026
|
+
}
|
|
1027
|
+
else if (x + rect.width > viewportSize.width - margin) {
|
|
1028
|
+
x = viewportSize.width - rect.width - margin;
|
|
1029
|
+
}
|
|
1030
|
+
// Push vertically
|
|
1031
|
+
if (y < margin) {
|
|
1032
|
+
y = margin;
|
|
1033
|
+
}
|
|
1034
|
+
else if (y + rect.height > viewportSize.height - margin) {
|
|
1035
|
+
y = viewportSize.height - rect.height - margin;
|
|
1036
|
+
}
|
|
1037
|
+
return { x, y };
|
|
1038
|
+
}
|
|
1039
|
+
_setTransformOrigin(position) {
|
|
1040
|
+
if (!this._transformOriginElement || !this._overlayElement) {
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
let transformOrigin = '';
|
|
1044
|
+
// Set horizontal transform origin
|
|
1045
|
+
switch (position.overlayX) {
|
|
1046
|
+
case 'start':
|
|
1047
|
+
transformOrigin += 'left';
|
|
1048
|
+
break;
|
|
1049
|
+
case 'center':
|
|
1050
|
+
transformOrigin += 'center';
|
|
1051
|
+
break;
|
|
1052
|
+
case 'end':
|
|
1053
|
+
transformOrigin += 'right';
|
|
1054
|
+
break;
|
|
1055
|
+
}
|
|
1056
|
+
transformOrigin += ' ';
|
|
1057
|
+
// Set vertical transform origin
|
|
1058
|
+
switch (position.overlayY) {
|
|
1059
|
+
case 'top':
|
|
1060
|
+
transformOrigin += 'top';
|
|
1061
|
+
break;
|
|
1062
|
+
case 'center':
|
|
1063
|
+
transformOrigin += 'center';
|
|
1064
|
+
break;
|
|
1065
|
+
case 'bottom':
|
|
1066
|
+
transformOrigin += 'bottom';
|
|
1067
|
+
break;
|
|
1068
|
+
}
|
|
1069
|
+
this._renderer.setStyle(this._transformOriginElement, 'transform-origin', transformOrigin);
|
|
1070
|
+
}
|
|
1071
|
+
_applyFlexibleDimensions(position, rect) {
|
|
1072
|
+
if (!this._overlayElement) {
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
const viewportSize = this._getViewportSize();
|
|
1076
|
+
const margin = this._viewportMargin;
|
|
1077
|
+
// Calculate available space
|
|
1078
|
+
const availableWidth = viewportSize.width - rect.x - margin;
|
|
1079
|
+
const availableHeight = viewportSize.height - rect.y - margin;
|
|
1080
|
+
// Apply flexible width if needed
|
|
1081
|
+
if (rect.width > availableWidth) {
|
|
1082
|
+
this._renderer.setStyle(this._overlayElement, 'max-width', `${availableWidth}px`);
|
|
1083
|
+
}
|
|
1084
|
+
// Apply flexible height if needed
|
|
1085
|
+
if (rect.height > availableHeight) {
|
|
1086
|
+
this._renderer.setStyle(this._overlayElement, 'max-height', `${availableHeight}px`);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
_setupEventListeners() {
|
|
1090
|
+
if (this._isDisposed) {
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
// Remove existing listeners
|
|
1094
|
+
this._removeEventListeners();
|
|
1095
|
+
// Set up resize listener
|
|
1096
|
+
this._resizeSubscription = () => {
|
|
1097
|
+
if (!this._isDisposed) {
|
|
1098
|
+
this.apply();
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
window.addEventListener('resize', this._resizeSubscription);
|
|
1102
|
+
// Set up scroll listener
|
|
1103
|
+
this._scrollSubscription = () => {
|
|
1104
|
+
if (!this._isDisposed) {
|
|
1105
|
+
this.apply();
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
window.addEventListener('scroll', this._scrollSubscription, true);
|
|
1109
|
+
}
|
|
1110
|
+
_removeEventListeners() {
|
|
1111
|
+
if (this._resizeSubscription) {
|
|
1112
|
+
window.removeEventListener('resize', this._resizeSubscription);
|
|
1113
|
+
this._resizeSubscription = undefined;
|
|
1114
|
+
}
|
|
1115
|
+
if (this._scrollSubscription) {
|
|
1116
|
+
window.removeEventListener('scroll', this._scrollSubscription, true);
|
|
1117
|
+
this._scrollSubscription = undefined;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
_getViewportSize() {
|
|
1121
|
+
return {
|
|
1122
|
+
width: window.innerWidth,
|
|
1123
|
+
height: window.innerHeight,
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
_getScrollableViewProperties() {
|
|
1127
|
+
const viewportSize = this._getViewportSize();
|
|
1128
|
+
return {
|
|
1129
|
+
size: viewportSize,
|
|
1130
|
+
scrollPosition: {
|
|
1131
|
+
top: window.pageYOffset || document.documentElement.scrollTop,
|
|
1132
|
+
left: window.pageXOffset || document.documentElement.scrollLeft,
|
|
1133
|
+
},
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* # FuiNoopScrollStrategy
|
|
1140
|
+
*
|
|
1141
|
+
* A scroll strategy that does nothing when the user scrolls.
|
|
1142
|
+
* This is useful for overlays that should remain in their position
|
|
1143
|
+
* regardless of scrolling.
|
|
1144
|
+
*/
|
|
1145
|
+
class FuiNoopScrollStrategy {
|
|
1146
|
+
enable() {
|
|
1147
|
+
// Intentionally empty
|
|
1148
|
+
}
|
|
1149
|
+
disable() {
|
|
1150
|
+
// Intentionally empty
|
|
1151
|
+
}
|
|
1152
|
+
attach(_overlayRef) {
|
|
1153
|
+
// Intentionally empty
|
|
1154
|
+
}
|
|
1155
|
+
detach() {
|
|
1156
|
+
// Intentionally empty
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* # FuiCloseScrollStrategy
|
|
1161
|
+
*
|
|
1162
|
+
* A scroll strategy that closes the overlay when the user scrolls.
|
|
1163
|
+
* This is useful for overlays like tooltips or dropdowns that should
|
|
1164
|
+
* disappear when the user scrolls away from the trigger element.
|
|
1165
|
+
*/
|
|
1166
|
+
class FuiCloseScrollStrategy {
|
|
1167
|
+
_threshold;
|
|
1168
|
+
_overlayRef;
|
|
1169
|
+
_scrollListener;
|
|
1170
|
+
_isEnabled = false;
|
|
1171
|
+
constructor(_threshold = 0) {
|
|
1172
|
+
this._threshold = _threshold;
|
|
1173
|
+
}
|
|
1174
|
+
enable() {
|
|
1175
|
+
if (this._isEnabled || !this._overlayRef) {
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
this._isEnabled = true;
|
|
1179
|
+
this._scrollListener = () => {
|
|
1180
|
+
if (this._overlayRef) {
|
|
1181
|
+
this._overlayRef.dispose();
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
window.addEventListener('scroll', this._scrollListener, true);
|
|
1185
|
+
}
|
|
1186
|
+
disable() {
|
|
1187
|
+
if (!this._isEnabled) {
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
this._isEnabled = false;
|
|
1191
|
+
this._removeScrollListener();
|
|
1192
|
+
}
|
|
1193
|
+
attach(overlayRef) {
|
|
1194
|
+
this._overlayRef = overlayRef;
|
|
1195
|
+
}
|
|
1196
|
+
detach() {
|
|
1197
|
+
this.disable();
|
|
1198
|
+
this._overlayRef = undefined;
|
|
1199
|
+
}
|
|
1200
|
+
_removeScrollListener() {
|
|
1201
|
+
if (this._scrollListener) {
|
|
1202
|
+
window.removeEventListener('scroll', this._scrollListener, true);
|
|
1203
|
+
this._scrollListener = undefined;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
/**
|
|
1208
|
+
* # FuiRepositionScrollStrategy
|
|
1209
|
+
*
|
|
1210
|
+
* A scroll strategy that repositions the overlay when the user scrolls.
|
|
1211
|
+
* This is useful for overlays that should follow their trigger element
|
|
1212
|
+
* as it moves due to scrolling.
|
|
1213
|
+
*/
|
|
1214
|
+
class FuiRepositionScrollStrategy {
|
|
1215
|
+
_scrollThrottle;
|
|
1216
|
+
_autoClose;
|
|
1217
|
+
_overlayRef;
|
|
1218
|
+
_scrollListener;
|
|
1219
|
+
_isEnabled = false;
|
|
1220
|
+
constructor(_scrollThrottle = 20, _autoClose = false) {
|
|
1221
|
+
this._scrollThrottle = _scrollThrottle;
|
|
1222
|
+
this._autoClose = _autoClose;
|
|
1223
|
+
}
|
|
1224
|
+
enable() {
|
|
1225
|
+
if (this._isEnabled || !this._overlayRef) {
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
this._isEnabled = true;
|
|
1229
|
+
let timeoutId = null;
|
|
1230
|
+
this._scrollListener = () => {
|
|
1231
|
+
if (timeoutId) {
|
|
1232
|
+
clearTimeout(timeoutId);
|
|
1233
|
+
}
|
|
1234
|
+
timeoutId = window.setTimeout(() => {
|
|
1235
|
+
if (this._overlayRef) {
|
|
1236
|
+
if (this._autoClose && this._isScrolledOutOfView()) {
|
|
1237
|
+
this._overlayRef.dispose();
|
|
1238
|
+
}
|
|
1239
|
+
else {
|
|
1240
|
+
this._overlayRef.updatePosition();
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}, this._scrollThrottle);
|
|
1244
|
+
};
|
|
1245
|
+
window.addEventListener('scroll', this._scrollListener, true);
|
|
1246
|
+
}
|
|
1247
|
+
disable() {
|
|
1248
|
+
if (!this._isEnabled) {
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
this._isEnabled = false;
|
|
1252
|
+
this._removeScrollListener();
|
|
1253
|
+
}
|
|
1254
|
+
attach(overlayRef) {
|
|
1255
|
+
this._overlayRef = overlayRef;
|
|
1256
|
+
}
|
|
1257
|
+
detach() {
|
|
1258
|
+
this.disable();
|
|
1259
|
+
this._overlayRef = undefined;
|
|
1260
|
+
}
|
|
1261
|
+
_removeScrollListener() {
|
|
1262
|
+
if (this._scrollListener) {
|
|
1263
|
+
window.removeEventListener('scroll', this._scrollListener, true);
|
|
1264
|
+
this._scrollListener = undefined;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
_isScrolledOutOfView() {
|
|
1268
|
+
// This is a simplified implementation
|
|
1269
|
+
// In a real implementation, you would check if the origin element
|
|
1270
|
+
// is still visible in the viewport
|
|
1271
|
+
return false;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
/**
|
|
1275
|
+
* # FuiBlockScrollStrategy
|
|
1276
|
+
*
|
|
1277
|
+
* A scroll strategy that prevents scrolling while the overlay is open.
|
|
1278
|
+
* This is useful for modal dialogs and other overlays that should
|
|
1279
|
+
* prevent interaction with the underlying content.
|
|
1280
|
+
*/
|
|
1281
|
+
class FuiBlockScrollStrategy {
|
|
1282
|
+
_document;
|
|
1283
|
+
_previousHTMLStyles = {};
|
|
1284
|
+
_previousBodyStyles = {};
|
|
1285
|
+
_isEnabled = false;
|
|
1286
|
+
constructor(_document) {
|
|
1287
|
+
this._document = _document;
|
|
1288
|
+
}
|
|
1289
|
+
enable() {
|
|
1290
|
+
if (this._isEnabled) {
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
this._isEnabled = true;
|
|
1294
|
+
const doc = this._document ?? document;
|
|
1295
|
+
const html = doc.documentElement;
|
|
1296
|
+
const body = doc.body;
|
|
1297
|
+
if (!html || !body) {
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
// Store previous styles
|
|
1301
|
+
this._previousHTMLStyles = {
|
|
1302
|
+
overflow: html.style.overflow || '',
|
|
1303
|
+
paddingRight: html.style.paddingRight || '',
|
|
1304
|
+
};
|
|
1305
|
+
this._previousBodyStyles = {
|
|
1306
|
+
overflow: body.style.overflow || '',
|
|
1307
|
+
paddingRight: body.style.paddingRight || '',
|
|
1308
|
+
};
|
|
1309
|
+
// Calculate scrollbar width
|
|
1310
|
+
const scrollbarWidth = this._getScrollbarWidth();
|
|
1311
|
+
// Apply blocking styles
|
|
1312
|
+
html.style.overflow = 'hidden';
|
|
1313
|
+
body.style.overflow = 'hidden';
|
|
1314
|
+
// Compensate for scrollbar width to prevent layout shift
|
|
1315
|
+
if (scrollbarWidth > 0) {
|
|
1316
|
+
html.style.paddingRight = `${scrollbarWidth}px`;
|
|
1317
|
+
body.style.paddingRight = `${scrollbarWidth}px`;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
disable() {
|
|
1321
|
+
if (!this._isEnabled) {
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
this._isEnabled = false;
|
|
1325
|
+
const doc = this._document ?? document;
|
|
1326
|
+
const html = doc.documentElement;
|
|
1327
|
+
const body = doc.body;
|
|
1328
|
+
if (!html || !body) {
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
// Restore previous styles
|
|
1332
|
+
html.style.overflow = this._previousHTMLStyles['overflow'];
|
|
1333
|
+
html.style.paddingRight = this._previousHTMLStyles['paddingRight'];
|
|
1334
|
+
body.style.overflow = this._previousBodyStyles['overflow'];
|
|
1335
|
+
body.style.paddingRight = this._previousBodyStyles['paddingRight'];
|
|
1336
|
+
// Clear stored styles
|
|
1337
|
+
this._previousHTMLStyles = {};
|
|
1338
|
+
this._previousBodyStyles = {};
|
|
1339
|
+
}
|
|
1340
|
+
attach(_overlayRef) {
|
|
1341
|
+
// Block scroll strategy doesn't need the overlay reference
|
|
1342
|
+
}
|
|
1343
|
+
detach() {
|
|
1344
|
+
this.disable();
|
|
1345
|
+
}
|
|
1346
|
+
_getScrollbarWidth() {
|
|
1347
|
+
const doc = this._document ?? document;
|
|
1348
|
+
// Create a temporary div to measure scrollbar width
|
|
1349
|
+
const outer = doc.createElement('div');
|
|
1350
|
+
outer.style.visibility = 'hidden';
|
|
1351
|
+
outer.style.overflow = 'scroll';
|
|
1352
|
+
// outer.style.msOverflowStyle = 'scrollbar'; // needed for WinJS apps
|
|
1353
|
+
doc.body.appendChild(outer);
|
|
1354
|
+
const inner = doc.createElement('div');
|
|
1355
|
+
outer.appendChild(inner);
|
|
1356
|
+
const scrollbarWidth = outer.offsetWidth - inner.offsetWidth;
|
|
1357
|
+
// Clean up
|
|
1358
|
+
outer.parentNode?.removeChild(outer);
|
|
1359
|
+
return scrollbarWidth;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
/**
|
|
1363
|
+
* Factory functions for creating scroll strategies
|
|
1364
|
+
*/
|
|
1365
|
+
const FUI_SCROLL_STRATEGIES = {
|
|
1366
|
+
/**
|
|
1367
|
+
* Creates a noop scroll strategy
|
|
1368
|
+
*/
|
|
1369
|
+
noop: () => new FuiNoopScrollStrategy(),
|
|
1370
|
+
/**
|
|
1371
|
+
* Creates a close scroll strategy
|
|
1372
|
+
*/
|
|
1373
|
+
close: (threshold = 0) => new FuiCloseScrollStrategy(threshold),
|
|
1374
|
+
/**
|
|
1375
|
+
* Creates a reposition scroll strategy
|
|
1376
|
+
*/
|
|
1377
|
+
reposition: (scrollThrottle = 20, autoClose = false) => new FuiRepositionScrollStrategy(scrollThrottle, autoClose),
|
|
1378
|
+
/**
|
|
1379
|
+
* Creates a block scroll strategy
|
|
1380
|
+
* @param doc Optional Document reference for SSR compatibility
|
|
1381
|
+
*/
|
|
1382
|
+
block: (doc) => new FuiBlockScrollStrategy(doc),
|
|
1383
|
+
};
|
|
1384
|
+
|
|
1385
|
+
/**
|
|
1386
|
+
* # FuiOverlayService
|
|
1387
|
+
*
|
|
1388
|
+
* Core service for creating and managing overlays. This service provides a
|
|
1389
|
+
* comprehensive overlay system similar to Angular Material's overlay service,
|
|
1390
|
+
* with support for positioning strategies, scroll strategies, backdrop handling,
|
|
1391
|
+
* and focus management.
|
|
1392
|
+
*
|
|
1393
|
+
* ## Features
|
|
1394
|
+
* - Create overlays with flexible positioning strategies
|
|
1395
|
+
* - Global and connected positioning support
|
|
1396
|
+
* - Multiple scroll strategies (reposition, close, block, noop)
|
|
1397
|
+
* - Backdrop support with click detection
|
|
1398
|
+
* - Z-index management for stacking overlays
|
|
1399
|
+
* - Focus management and restoration
|
|
1400
|
+
* - Keyboard event handling
|
|
1401
|
+
* - Responsive positioning with collision detection
|
|
1402
|
+
*
|
|
1403
|
+
* ## Usage
|
|
1404
|
+
*
|
|
1405
|
+
* ### Basic Overlay
|
|
1406
|
+
* ```typescript
|
|
1407
|
+
* const overlayRef = this.overlay.create({
|
|
1408
|
+
* positionStrategy: this.overlay.position()
|
|
1409
|
+
* .global()
|
|
1410
|
+
* .centerHorizontally()
|
|
1411
|
+
* .centerVertically(),
|
|
1412
|
+
* hasBackdrop: true
|
|
1413
|
+
* });
|
|
1414
|
+
*
|
|
1415
|
+
* overlayRef.attach(myComponent);
|
|
1416
|
+
* ```
|
|
1417
|
+
*
|
|
1418
|
+
* ### Connected Overlay (Tooltip/Dropdown)
|
|
1419
|
+
* ```typescript
|
|
1420
|
+
* const overlayRef = this.overlay.create({
|
|
1421
|
+
* positionStrategy: this.overlay.position()
|
|
1422
|
+
* .connectedTo(this.triggerElement)
|
|
1423
|
+
* .withPositions([{
|
|
1424
|
+
* originX: 'center',
|
|
1425
|
+
* originY: 'bottom',
|
|
1426
|
+
* overlayX: 'center',
|
|
1427
|
+
* overlayY: 'top',
|
|
1428
|
+
* offsetY: 8
|
|
1429
|
+
* }]),
|
|
1430
|
+
* scrollStrategy: this.overlay.scrollStrategies.reposition()
|
|
1431
|
+
* });
|
|
1432
|
+
* ```
|
|
1433
|
+
*/
|
|
1434
|
+
/** Base z-index for overlays */
|
|
1435
|
+
const OVERLAY_BASE_Z_INDEX = 1000;
|
|
1436
|
+
/** Z-index increment for each new overlay */
|
|
1437
|
+
const OVERLAY_Z_INDEX_INCREMENT = 10;
|
|
1438
|
+
class FuiOverlayService {
|
|
1439
|
+
_document = inject(DOCUMENT);
|
|
1440
|
+
_rendererFactory = inject(RendererFactory2);
|
|
1441
|
+
_environmentInjector = inject(EnvironmentInjector);
|
|
1442
|
+
_renderer;
|
|
1443
|
+
_overlayContainer;
|
|
1444
|
+
_nextUniqueId = 0;
|
|
1445
|
+
_activeOverlays = new Set();
|
|
1446
|
+
_currentZIndex = OVERLAY_BASE_Z_INDEX;
|
|
1447
|
+
constructor() {
|
|
1448
|
+
this._renderer = this._rendererFactory.createRenderer(null, null);
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Creates a new overlay with the given configuration
|
|
1452
|
+
*/
|
|
1453
|
+
create(config = {}) {
|
|
1454
|
+
const overlayElement = this._createOverlayElement();
|
|
1455
|
+
// Calculate z-index for this overlay (increments for each new overlay)
|
|
1456
|
+
const zIndex = this._getNextZIndex();
|
|
1457
|
+
const overlayRef = new FuiOverlayRefImpl(overlayElement, this._applyConfigDefaults(config), this._renderer, `fui-overlay-${this._nextUniqueId++}`, zIndex);
|
|
1458
|
+
// Attach to container
|
|
1459
|
+
this._attachToContainer(overlayElement);
|
|
1460
|
+
// Track active overlay
|
|
1461
|
+
this._activeOverlays.add(overlayRef);
|
|
1462
|
+
// Set up disposal cleanup
|
|
1463
|
+
overlayRef.detachments.subscribe(() => {
|
|
1464
|
+
this._activeOverlays.delete(overlayRef);
|
|
1465
|
+
this._recalculateBaseZIndex();
|
|
1466
|
+
});
|
|
1467
|
+
return overlayRef;
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* Gets the next available z-index for a new overlay
|
|
1471
|
+
*/
|
|
1472
|
+
_getNextZIndex() {
|
|
1473
|
+
this._currentZIndex += OVERLAY_Z_INDEX_INCREMENT;
|
|
1474
|
+
return this._currentZIndex;
|
|
1475
|
+
}
|
|
1476
|
+
/**
|
|
1477
|
+
* Recalculates the base z-index when overlays are disposed
|
|
1478
|
+
* Resets to base if no overlays are active
|
|
1479
|
+
*/
|
|
1480
|
+
_recalculateBaseZIndex() {
|
|
1481
|
+
if (this._activeOverlays.size === 0) {
|
|
1482
|
+
this._currentZIndex = OVERLAY_BASE_Z_INDEX;
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
/**
|
|
1486
|
+
* Gets the position builder for creating position strategies
|
|
1487
|
+
*/
|
|
1488
|
+
position() {
|
|
1489
|
+
return new FuiOverlayPositionBuilder(this._renderer);
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Gets the scroll strategies factory
|
|
1493
|
+
*/
|
|
1494
|
+
get scrollStrategies() {
|
|
1495
|
+
return FUI_SCROLL_STRATEGIES;
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1498
|
+
* Gets all currently active overlays
|
|
1499
|
+
*/
|
|
1500
|
+
getActiveOverlays() {
|
|
1501
|
+
return Array.from(this._activeOverlays);
|
|
1502
|
+
}
|
|
1503
|
+
/**
|
|
1504
|
+
* Disposes all active overlays
|
|
1505
|
+
*/
|
|
1506
|
+
disposeAll() {
|
|
1507
|
+
const overlays = Array.from(this._activeOverlays);
|
|
1508
|
+
overlays.forEach((overlay) => {
|
|
1509
|
+
overlay.dispose();
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
_createOverlayElement() {
|
|
1513
|
+
const overlayElement = this._renderer.createElement('div');
|
|
1514
|
+
this._renderer.addClass(overlayElement, 'fui-overlay-pane');
|
|
1515
|
+
this._renderer.setStyle(overlayElement, 'pointer-events', 'auto');
|
|
1516
|
+
return overlayElement;
|
|
1517
|
+
}
|
|
1518
|
+
_attachToContainer(overlayElement) {
|
|
1519
|
+
const container = this._getOverlayContainer();
|
|
1520
|
+
this._renderer.appendChild(container, overlayElement);
|
|
1521
|
+
}
|
|
1522
|
+
_getOverlayContainer() {
|
|
1523
|
+
if (!this._overlayContainer) {
|
|
1524
|
+
this._overlayContainer = createComponent(FuiOverlayContainerComponent, {
|
|
1525
|
+
environmentInjector: this._environmentInjector,
|
|
1526
|
+
});
|
|
1527
|
+
// Manually trigger ngOnInit to ensure container is attached to body
|
|
1528
|
+
this._overlayContainer.instance.ngOnInit();
|
|
1529
|
+
}
|
|
1530
|
+
return this._overlayContainer.instance.getContainerElement();
|
|
1531
|
+
}
|
|
1532
|
+
_applyConfigDefaults(config) {
|
|
1533
|
+
return {
|
|
1534
|
+
hasBackdrop: false,
|
|
1535
|
+
backdropClass: 'fui-overlay-backdrop',
|
|
1536
|
+
backdropClickBehavior: 'close',
|
|
1537
|
+
panelClass: '',
|
|
1538
|
+
scrollStrategy: this.scrollStrategies.noop(),
|
|
1539
|
+
direction: 'ltr',
|
|
1540
|
+
disposeOnNavigation: true,
|
|
1541
|
+
...config,
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: FuiOverlayService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1545
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: FuiOverlayService, providedIn: 'root' });
|
|
1546
|
+
}
|
|
1547
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: FuiOverlayService, decorators: [{
|
|
1548
|
+
type: Injectable,
|
|
1549
|
+
args: [{
|
|
1550
|
+
providedIn: 'root',
|
|
1551
|
+
}]
|
|
1552
|
+
}], ctorParameters: () => [] });
|
|
1553
|
+
/**
|
|
1554
|
+
* # FuiOverlayPositionBuilder
|
|
1555
|
+
*
|
|
1556
|
+
* Builder class for creating position strategies. Provides a fluent API
|
|
1557
|
+
* for configuring overlay positioning.
|
|
1558
|
+
*/
|
|
1559
|
+
class FuiOverlayPositionBuilder {
|
|
1560
|
+
_renderer;
|
|
1561
|
+
constructor(_renderer) {
|
|
1562
|
+
this._renderer = _renderer;
|
|
1563
|
+
}
|
|
1564
|
+
/**
|
|
1565
|
+
* Creates a global position strategy
|
|
1566
|
+
*/
|
|
1567
|
+
global(options = {}) {
|
|
1568
|
+
return new FuiGlobalPositionStrategy(this._renderer, options);
|
|
1569
|
+
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Creates a connected position strategy
|
|
1572
|
+
*/
|
|
1573
|
+
connectedTo(origin, positions) {
|
|
1574
|
+
const defaultPositions = [
|
|
1575
|
+
{
|
|
1576
|
+
originX: 'start',
|
|
1577
|
+
originY: 'bottom',
|
|
1578
|
+
overlayX: 'start',
|
|
1579
|
+
overlayY: 'top',
|
|
1580
|
+
},
|
|
1581
|
+
{
|
|
1582
|
+
originX: 'start',
|
|
1583
|
+
originY: 'top',
|
|
1584
|
+
overlayX: 'start',
|
|
1585
|
+
overlayY: 'bottom',
|
|
1586
|
+
},
|
|
1587
|
+
];
|
|
1588
|
+
const options = {
|
|
1589
|
+
origin,
|
|
1590
|
+
positions: positions ?? defaultPositions,
|
|
1591
|
+
};
|
|
1592
|
+
return new FuiConnectedPositionStrategy(this._renderer, options);
|
|
1593
|
+
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Creates a flexible connected position strategy with common tooltip positions
|
|
1596
|
+
*/
|
|
1597
|
+
flexibleConnectedTo(origin, preferredPosition = 'top') {
|
|
1598
|
+
const positions = this._getFlexiblePositions(preferredPosition);
|
|
1599
|
+
const options = {
|
|
1600
|
+
origin,
|
|
1601
|
+
positions,
|
|
1602
|
+
withPush: true,
|
|
1603
|
+
withFlexibleDimensions: true,
|
|
1604
|
+
viewportMargin: 8,
|
|
1605
|
+
};
|
|
1606
|
+
return new FuiConnectedPositionStrategy(this._renderer, options);
|
|
1607
|
+
}
|
|
1608
|
+
_getFlexiblePositions(preferredPosition) {
|
|
1609
|
+
const positions = [];
|
|
1610
|
+
switch (preferredPosition) {
|
|
1611
|
+
case 'top':
|
|
1612
|
+
positions.push({ originX: 'center', originY: 'top', overlayX: 'center', overlayY: 'bottom', offsetY: -8 }, { originX: 'center', originY: 'bottom', overlayX: 'center', overlayY: 'top', offsetY: 8 }, { originX: 'end', originY: 'center', overlayX: 'start', overlayY: 'center', offsetX: 8 }, { originX: 'start', originY: 'center', overlayX: 'end', overlayY: 'center', offsetX: -8 });
|
|
1613
|
+
break;
|
|
1614
|
+
case 'bottom':
|
|
1615
|
+
positions.push({ originX: 'center', originY: 'bottom', overlayX: 'center', overlayY: 'top', offsetY: 8 }, { originX: 'center', originY: 'top', overlayX: 'center', overlayY: 'bottom', offsetY: -8 }, { originX: 'end', originY: 'center', overlayX: 'start', overlayY: 'center', offsetX: 8 }, { originX: 'start', originY: 'center', overlayX: 'end', overlayY: 'center', offsetX: -8 });
|
|
1616
|
+
break;
|
|
1617
|
+
case 'left':
|
|
1618
|
+
positions.push({ originX: 'start', originY: 'center', overlayX: 'end', overlayY: 'center', offsetX: -8 }, { originX: 'end', originY: 'center', overlayX: 'start', overlayY: 'center', offsetX: 8 }, { originX: 'center', originY: 'top', overlayX: 'center', overlayY: 'bottom', offsetY: -8 }, { originX: 'center', originY: 'bottom', overlayX: 'center', overlayY: 'top', offsetY: 8 });
|
|
1619
|
+
break;
|
|
1620
|
+
case 'right':
|
|
1621
|
+
positions.push({ originX: 'end', originY: 'center', overlayX: 'start', overlayY: 'center', offsetX: 8 }, { originX: 'start', originY: 'center', overlayX: 'end', overlayY: 'center', offsetX: -8 }, { originX: 'center', originY: 'top', overlayX: 'center', overlayY: 'bottom', offsetY: -8 }, { originX: 'center', originY: 'bottom', overlayX: 'center', overlayY: 'top', offsetY: 8 });
|
|
1622
|
+
break;
|
|
1623
|
+
}
|
|
1624
|
+
return positions;
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
/**
|
|
1629
|
+
* # Portal Service
|
|
1630
|
+
*
|
|
1631
|
+
* A service for dynamically moving DOM elements to different locations in the DOM tree,
|
|
1632
|
+
* particularly useful for moving elements to the document body to avoid clipping issues
|
|
1633
|
+
* with parent containers that have overflow: hidden.
|
|
1634
|
+
*
|
|
1635
|
+
* ## Features
|
|
1636
|
+
* - Move elements to document body or any other container
|
|
1637
|
+
* - Maintain element references and event listeners
|
|
1638
|
+
* - Proper cleanup when detaching
|
|
1639
|
+
* - Support for multiple portal outlets
|
|
1640
|
+
*
|
|
1641
|
+
* ## Usage
|
|
1642
|
+
*
|
|
1643
|
+
* ```typescript
|
|
1644
|
+
* constructor(private portalService: PortalService) {}
|
|
1645
|
+
*
|
|
1646
|
+
* openMenu() {
|
|
1647
|
+
* const menuElement = this.menuElementRef.nativeElement;
|
|
1648
|
+
* this.portalAttachment = this.portalService.attachToBody(menuElement);
|
|
1649
|
+
* }
|
|
1650
|
+
*
|
|
1651
|
+
* closeMenu() {
|
|
1652
|
+
* if (this.portalAttachment) {
|
|
1653
|
+
* this.portalAttachment.detach();
|
|
1654
|
+
* this.portalAttachment = null;
|
|
1655
|
+
* }
|
|
1656
|
+
* }
|
|
1657
|
+
* ```
|
|
1658
|
+
*/
|
|
1659
|
+
class PortalService {
|
|
1660
|
+
_document = inject(DOCUMENT$1);
|
|
1661
|
+
_renderer;
|
|
1662
|
+
_rendererFactory = inject(RendererFactory2);
|
|
1663
|
+
constructor() {
|
|
1664
|
+
this._renderer = this._rendererFactory.createRenderer(null, null);
|
|
1665
|
+
}
|
|
1666
|
+
/**
|
|
1667
|
+
* Attaches an element to the document body
|
|
1668
|
+
* @param element The element to attach
|
|
1669
|
+
* @returns PortalAttachment with detach function
|
|
1670
|
+
*/
|
|
1671
|
+
attachToBody(element) {
|
|
1672
|
+
return this.attachToContainer(element, this._document.body);
|
|
1673
|
+
}
|
|
1674
|
+
/**
|
|
1675
|
+
* Attaches an element to a specific container
|
|
1676
|
+
* @param element The element to attach
|
|
1677
|
+
* @param container The container to attach to
|
|
1678
|
+
* @returns PortalAttachment with detach function
|
|
1679
|
+
*/
|
|
1680
|
+
attachToContainer(element, container) {
|
|
1681
|
+
// Store the original parent and next sibling for restoration
|
|
1682
|
+
const originalParent = element.parentElement;
|
|
1683
|
+
const originalNextSibling = element.nextSibling;
|
|
1684
|
+
// Move the element to the container
|
|
1685
|
+
this._renderer.appendChild(container, element);
|
|
1686
|
+
// Return attachment object with detach function
|
|
1687
|
+
return {
|
|
1688
|
+
element,
|
|
1689
|
+
detach: () => {
|
|
1690
|
+
// Restore the element to its original position
|
|
1691
|
+
if (originalParent) {
|
|
1692
|
+
if (originalNextSibling) {
|
|
1693
|
+
this._renderer.insertBefore(originalParent, element, originalNextSibling);
|
|
1694
|
+
}
|
|
1695
|
+
else {
|
|
1696
|
+
this._renderer.appendChild(originalParent, element);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
},
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
/**
|
|
1703
|
+
* Creates a portal outlet element and attaches it to the document body
|
|
1704
|
+
* @param className Optional CSS class for the outlet
|
|
1705
|
+
* @returns The created outlet element
|
|
1706
|
+
*/
|
|
1707
|
+
createBodyOutlet(className) {
|
|
1708
|
+
const outlet = this._renderer.createElement('div');
|
|
1709
|
+
if (className) {
|
|
1710
|
+
this._renderer.addClass(outlet, className);
|
|
1711
|
+
}
|
|
1712
|
+
// Add some default styles to ensure proper positioning
|
|
1713
|
+
this._renderer.setStyle(outlet, 'position', 'absolute');
|
|
1714
|
+
this._renderer.setStyle(outlet, 'top', '0');
|
|
1715
|
+
this._renderer.setStyle(outlet, 'left', '0');
|
|
1716
|
+
this._renderer.setStyle(outlet, 'pointer-events', 'none');
|
|
1717
|
+
this._renderer.setStyle(outlet, 'z-index', '1000');
|
|
1718
|
+
this._renderer.appendChild(this._document.body, outlet);
|
|
1719
|
+
return outlet;
|
|
1720
|
+
}
|
|
1721
|
+
/**
|
|
1722
|
+
* Removes a portal outlet from the document
|
|
1723
|
+
* @param outlet The outlet element to remove
|
|
1724
|
+
*/
|
|
1725
|
+
removeOutlet(outlet) {
|
|
1726
|
+
if (outlet.parentElement) {
|
|
1727
|
+
this._renderer.removeChild(outlet.parentElement, outlet);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
/**
|
|
1731
|
+
* Checks if an element is currently attached to a portal
|
|
1732
|
+
* @param element The element to check
|
|
1733
|
+
* @returns True if the element is attached to the body or not in its original position
|
|
1734
|
+
*/
|
|
1735
|
+
isAttachedToPortal(element) {
|
|
1736
|
+
// Simple check: if the element's parent is the body, it's likely in a portal
|
|
1737
|
+
// This is a heuristic and might need refinement based on specific use cases
|
|
1738
|
+
return element.parentElement === this._document.body;
|
|
1739
|
+
}
|
|
1740
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: PortalService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1741
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: PortalService, providedIn: 'root' });
|
|
1742
|
+
}
|
|
1743
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.6", ngImport: i0, type: PortalService, decorators: [{
|
|
1744
|
+
type: Injectable,
|
|
1745
|
+
args: [{
|
|
1746
|
+
providedIn: 'root',
|
|
1747
|
+
}]
|
|
1748
|
+
}], ctorParameters: () => [] });
|
|
1749
|
+
|
|
1750
|
+
// Core overlay exports
|
|
1751
|
+
|
|
1752
|
+
/**
|
|
1753
|
+
* Generated bundle index. Do not edit.
|
|
1754
|
+
*/
|
|
1755
|
+
|
|
1756
|
+
export { FUI_OVERLAY_CONTAINER, FUI_OVERLAY_SCROLL_STRATEGY, FUI_SCROLL_STRATEGIES, FuiBlockScrollStrategy, FuiCloseScrollStrategy, FuiConnectedPositionStrategy, FuiGlobalPositionStrategy, FuiNoopScrollStrategy, FuiOverlayContainerComponent, FuiOverlayPositionBuilder, FuiOverlayRefImpl, FuiOverlayService, FuiRepositionScrollStrategy, PortalService };
|
|
1757
|
+
//# sourceMappingURL=raintonic-formaui-cdk-overlay.mjs.map
|