@aquera/nile-elements 1.2.8-beta-1.7 → 1.2.8-beta-1.9
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/demo/index.html +54 -51
- package/dist/index.cjs.js +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.js +285 -274
- package/dist/nile-auto-complete/index.cjs.js +1 -1
- package/dist/nile-auto-complete/index.esm.js +1 -1
- package/dist/nile-auto-complete/nile-auto-complete.cjs.js +1 -1
- package/dist/nile-auto-complete/nile-auto-complete.cjs.js.map +1 -1
- package/dist/nile-auto-complete/nile-auto-complete.esm.js +17 -13
- package/dist/nile-auto-complete/nile-auto-complete.test.cjs.js +1 -1
- package/dist/nile-auto-complete/nile-auto-complete.test.cjs.js.map +1 -1
- package/dist/nile-auto-complete/nile-auto-complete.test.esm.js +1 -1
- package/dist/nile-auto-complete/portal-manager.cjs.js +2 -0
- package/dist/nile-auto-complete/portal-manager.cjs.js.map +1 -0
- package/dist/nile-auto-complete/portal-manager.esm.js +1 -0
- package/dist/nile-auto-complete/portal-utils.cjs.js +2 -0
- package/dist/nile-auto-complete/portal-utils.cjs.js.map +1 -0
- package/dist/nile-auto-complete/portal-utils.esm.js +1 -0
- package/dist/nile-chip/index.cjs.js +1 -1
- package/dist/nile-chip/index.esm.js +1 -1
- package/dist/nile-chip/nile-chip.cjs.js +1 -1
- package/dist/nile-chip/nile-chip.cjs.js.map +1 -1
- package/dist/nile-chip/nile-chip.esm.js +14 -7
- package/dist/nile-chip/nile-chip.test.cjs.js +1 -1
- package/dist/nile-chip/nile-chip.test.cjs.js.map +1 -1
- package/dist/nile-chip/nile-chip.test.esm.js +1 -1
- package/dist/nile-lite-tooltip/nile-lite-tooltip.cjs.js +1 -1
- package/dist/nile-lite-tooltip/nile-lite-tooltip.cjs.js.map +1 -1
- package/dist/nile-lite-tooltip/nile-lite-tooltip.esm.js +1 -1
- package/dist/src/nile-auto-complete/nile-auto-complete.d.ts +18 -1
- package/dist/src/nile-auto-complete/nile-auto-complete.js +131 -9
- package/dist/src/nile-auto-complete/nile-auto-complete.js.map +1 -1
- package/dist/src/nile-auto-complete/portal-manager.d.ts +41 -0
- package/dist/src/nile-auto-complete/portal-manager.js +308 -0
- package/dist/src/nile-auto-complete/portal-manager.js.map +1 -0
- package/dist/src/nile-auto-complete/portal-utils.d.ts +31 -0
- package/dist/src/nile-auto-complete/portal-utils.js +166 -0
- package/dist/src/nile-auto-complete/portal-utils.js.map +1 -0
- package/dist/src/nile-chip/nile-chip.d.ts +7 -0
- package/dist/src/nile-chip/nile-chip.js +53 -10
- package/dist/src/nile-chip/nile-chip.js.map +1 -1
- package/dist/src/nile-lite-tooltip/nile-lite-tooltip.js.map +1 -1
- package/dist/src/version.js +2 -2
- package/dist/src/version.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/nile-auto-complete/nile-auto-complete.ts +148 -13
- package/src/nile-auto-complete/portal-manager.ts +410 -0
- package/src/nile-auto-complete/portal-utils.ts +221 -0
- package/src/nile-chip/nile-chip.ts +54 -12
- package/src/nile-lite-tooltip/nile-lite-tooltip.ts +4 -3
- package/vscode-html-custom-data.json +17 -2
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Webcomponent nile-elements following open-wc recommendations",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "nile-elements",
|
|
6
|
-
"version": "1.2.8-beta-1.
|
|
6
|
+
"version": "1.2.8-beta-1.9",
|
|
7
7
|
"main": "dist/src/index.js",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"module": "dist/src/index.js",
|
|
@@ -10,8 +10,12 @@ import { styles } from './nile-auto-complete.css';
|
|
|
10
10
|
import NileElement from '../internal/nile-element';
|
|
11
11
|
import type { CSSResultGroup, PropertyValues } from 'lit';
|
|
12
12
|
import { NileDropdown } from '../nile-dropdown';
|
|
13
|
+
import { watch } from '../internal/watch';
|
|
14
|
+
import { AutoCompletePortalManager } from './portal-manager';
|
|
15
|
+
import { NileInput } from '../nile-input';
|
|
13
16
|
|
|
14
17
|
import { virtualize } from '@lit-labs/virtualizer/virtualize.js';
|
|
18
|
+
import { unsafeHTML } from 'lit/directives/unsafe-html.js';
|
|
15
19
|
|
|
16
20
|
// Define the custom element 'nile-auto-complete'
|
|
17
21
|
@customElement('nile-auto-complete')
|
|
@@ -21,12 +25,22 @@ export class NileAutoComplete extends NileElement {
|
|
|
21
25
|
|
|
22
26
|
@query('nile-dropdown') dropdownElement: NileDropdown;
|
|
23
27
|
|
|
28
|
+
@query('nile-input') inputElement: NileInput;
|
|
29
|
+
|
|
24
30
|
// Define component properties
|
|
25
31
|
|
|
26
32
|
@property({ type: Boolean }) disabled: boolean = false;
|
|
27
33
|
|
|
28
34
|
@property({ type: Boolean }) isDropdownOpen: boolean = false;
|
|
29
35
|
|
|
36
|
+
/**
|
|
37
|
+
* When true, the dropdown menu will be appended to the document body instead of the parent container.
|
|
38
|
+
* This is useful when the parent has overflow: hidden, clip-path, or transform applied.
|
|
39
|
+
*/
|
|
40
|
+
@property({ type: Boolean, reflect: true }) portal = false;
|
|
41
|
+
|
|
42
|
+
private readonly portalManager = new AutoCompletePortalManager(this);
|
|
43
|
+
|
|
30
44
|
@property({ type: Boolean }) enableVirtualScroll: boolean = false;
|
|
31
45
|
|
|
32
46
|
@property({ type: Boolean }) openOnFocus: boolean = false;
|
|
@@ -54,6 +68,16 @@ export class NileAutoComplete extends NileElement {
|
|
|
54
68
|
connectedCallback() {
|
|
55
69
|
super.connectedCallback();
|
|
56
70
|
this.renderItemFunction=(item:any)=>item;
|
|
71
|
+
this.handleDocumentFocusIn = this.handleDocumentFocusIn.bind(this);
|
|
72
|
+
this.handleDocumentMouseDown = this.handleDocumentMouseDown.bind(this);
|
|
73
|
+
this.handleWindowResize = this.handleWindowResize.bind(this);
|
|
74
|
+
this.handleWindowScroll = this.handleWindowScroll.bind(this);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
disconnectedCallback() {
|
|
78
|
+
super.disconnectedCallback();
|
|
79
|
+
this.removeOpenListeners();
|
|
80
|
+
this.portalManager.cleanupPortalAppend();
|
|
57
81
|
}
|
|
58
82
|
|
|
59
83
|
protected updated(changedProperties: PropertyValues): void {
|
|
@@ -61,13 +85,102 @@ export class NileAutoComplete extends NileElement {
|
|
|
61
85
|
if (changedProperties.has('allMenuItems')){
|
|
62
86
|
this.menuItems = this.applyFilter(this.allMenuItems,this.filterFunction);
|
|
63
87
|
this.setVirtualMenuWidth();
|
|
88
|
+
if (this.portal && this.isDropdownOpen) {
|
|
89
|
+
this.portalManager.updatePortalOptions();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (changedProperties.has('isDropdownOpen')) {
|
|
93
|
+
this.menuItems = this.applyFilter(this.allMenuItems,this.filterFunction);
|
|
94
|
+
this.handleDropdownOpenChange();
|
|
64
95
|
}
|
|
65
|
-
if (changedProperties.has('isDropdownOpen')) this.menuItems = this.applyFilter(this.allMenuItems,this.filterFunction);
|
|
66
96
|
if (changedProperties.has('value')){
|
|
67
97
|
this.menuItems = this.applyFilter(this.allMenuItems,this.filterFunction);
|
|
98
|
+
if (this.portal && this.isDropdownOpen) {
|
|
99
|
+
this.portalManager.updatePortalOptions();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (changedProperties.has('portal')) {
|
|
103
|
+
this.handlePortalChange();
|
|
68
104
|
}
|
|
69
105
|
}
|
|
70
106
|
|
|
107
|
+
@watch('portal', { waitUntilFirstUpdate: true })
|
|
108
|
+
handlePortalChange(): void {
|
|
109
|
+
if (this.isDropdownOpen) {
|
|
110
|
+
if (this.portal) {
|
|
111
|
+
this.portalManager.setupPortalAppend();
|
|
112
|
+
} else {
|
|
113
|
+
this.portalManager.cleanupPortalAppend();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private handleDropdownOpenChange(): void {
|
|
119
|
+
if (this.isDropdownOpen) {
|
|
120
|
+
this.addOpenListeners();
|
|
121
|
+
if (this.portal) {
|
|
122
|
+
this.portalManager.setupPortalAppend();
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
this.removeOpenListeners();
|
|
126
|
+
if (this.portal) {
|
|
127
|
+
this.portalManager.cleanupPortalAppend();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private addOpenListeners(): void {
|
|
133
|
+
document.addEventListener('focusin', this.handleDocumentFocusIn);
|
|
134
|
+
document.addEventListener('mousedown', this.handleDocumentMouseDown);
|
|
135
|
+
|
|
136
|
+
if (this.portal) {
|
|
137
|
+
window.addEventListener('resize', this.handleWindowResize);
|
|
138
|
+
window.addEventListener('scroll', this.handleWindowScroll, true);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private removeOpenListeners(): void {
|
|
143
|
+
document.removeEventListener('focusin', this.handleDocumentFocusIn);
|
|
144
|
+
document.removeEventListener('mousedown', this.handleDocumentMouseDown);
|
|
145
|
+
window.removeEventListener('resize', this.handleWindowResize);
|
|
146
|
+
window.removeEventListener('scroll', this.handleWindowScroll, true);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private handleDocumentFocusIn(event: FocusEvent) {
|
|
150
|
+
if (!this.isDropdownOpen) return;
|
|
151
|
+
const path = event.composedPath();
|
|
152
|
+
const hitSelf = path.includes(this);
|
|
153
|
+
const hitDropdown = this.dropdownElement && path.includes(this.dropdownElement);
|
|
154
|
+
const hitPortalAppend = this.portal && this.portalManager.portalContainerElement && path.includes(this.portalManager.portalContainerElement);
|
|
155
|
+
|
|
156
|
+
if (!hitSelf && !hitDropdown && !hitPortalAppend) {
|
|
157
|
+
this.isDropdownOpen = false;
|
|
158
|
+
this.dropdownElement?.hide();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private handleDocumentMouseDown(event: MouseEvent) {
|
|
163
|
+
if (!this.isDropdownOpen) return;
|
|
164
|
+
|
|
165
|
+
const path = event.composedPath();
|
|
166
|
+
const hitSelf = path.includes(this);
|
|
167
|
+
const hitDropdown = this.dropdownElement && path.includes(this.dropdownElement);
|
|
168
|
+
const hitPortalAppend = this.portal && this.portalManager.portalContainerElement && path.includes(this.portalManager.portalContainerElement);
|
|
169
|
+
|
|
170
|
+
if (!hitSelf && !hitDropdown && !hitPortalAppend) {
|
|
171
|
+
this.isDropdownOpen = false;
|
|
172
|
+
this.dropdownElement?.hide();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private handleWindowResize = (): void => {
|
|
177
|
+
this.portalManager.updatePortalAppendPosition();
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
private handleWindowScroll = (): void => {
|
|
181
|
+
this.portalManager.updatePortalAppendPosition();
|
|
182
|
+
};
|
|
183
|
+
|
|
71
184
|
public render(): TemplateResult {
|
|
72
185
|
const content=this.enableVirtualScroll?this.getVirtualizedContent():this.getContent();
|
|
73
186
|
return html`
|
|
@@ -96,7 +209,7 @@ export class NileAutoComplete extends NileElement {
|
|
|
96
209
|
|
|
97
210
|
getVirtualizedContent():TemplateResult{
|
|
98
211
|
return html`
|
|
99
|
-
<nile-menu class="virtualized__menu" @nile-select=${this.handleSelect} id="content-menu" exportparts="menu__items-wrapper:options__wrapper">
|
|
212
|
+
<nile-menu class="virtualized__menu" @nile-select=${this.handleSelect} id="content-menu" exportparts="menu__items-wrapper:options__wrapper" style=${this.portal ? 'display: none;' : ''}>
|
|
100
213
|
${virtualize({
|
|
101
214
|
items: this.menuItems,
|
|
102
215
|
renderItem: (item:any):TemplateResult=>this.getItemRenderFunction(item),
|
|
@@ -108,21 +221,34 @@ export class NileAutoComplete extends NileElement {
|
|
|
108
221
|
|
|
109
222
|
getContent():TemplateResult{
|
|
110
223
|
return html`
|
|
111
|
-
<nile-menu id="content-menu" @nile-select=${this.handleSelect} exportparts="menu__items-wrapper:options__wrapper">
|
|
224
|
+
<nile-menu id="content-menu" @nile-select=${this.handleSelect} exportparts="menu__items-wrapper:options__wrapper" style=${this.portal ? 'display: none;' : ''}>
|
|
112
225
|
${this.menuItems.map((item: any) => this.getItemRenderFunction(item))}
|
|
113
226
|
</nile-menu>`
|
|
114
227
|
}
|
|
115
228
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
229
|
+
getItemRenderFunction(item: any): TemplateResult {
|
|
230
|
+
const value = this.renderItemFunction(item);
|
|
231
|
+
const hasTooltip = !!item.tooltip;
|
|
232
|
+
const shouldShowTooltip =
|
|
233
|
+
hasTooltip && (!item.tooltip.for || item.tooltip.for === 'menu');
|
|
234
|
+
const tooltipContent = hasTooltip ? item.tooltip.content : value;
|
|
235
|
+
|
|
236
|
+
return shouldShowTooltip
|
|
237
|
+
? html`
|
|
238
|
+
<nile-menu-item value=${value}>
|
|
239
|
+
<nile-lite-tooltip allowHTML .content=${tooltipContent}>
|
|
240
|
+
<span class="menu-item-text">${unsafeHTML(value)}</span>
|
|
241
|
+
</nile-lite-tooltip>
|
|
242
|
+
</nile-menu-item>
|
|
243
|
+
`
|
|
244
|
+
: html`
|
|
245
|
+
<nile-menu-item value=${value}>${unsafeHTML(value)}</nile-menu-item>
|
|
246
|
+
`;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
124
250
|
|
|
125
|
-
|
|
251
|
+
handleSelect(event: CustomEvent) {
|
|
126
252
|
this.value = event.detail.value;
|
|
127
253
|
this.emit('nile-complete', { value: event.detail.value });
|
|
128
254
|
this.isDropdownOpen = false;
|
|
@@ -148,7 +274,12 @@ export class NileAutoComplete extends NileElement {
|
|
|
148
274
|
this.menuItems = this.applyFilter(this.allMenuItems,this.filterFunction);
|
|
149
275
|
|
|
150
276
|
this.isDropdownOpen = this.menuItems.length > 0;
|
|
151
|
-
if (this.isDropdownOpen)
|
|
277
|
+
if (this.isDropdownOpen) {
|
|
278
|
+
this.dropdownElement?.show();
|
|
279
|
+
if (this.portal) {
|
|
280
|
+
this.portalManager.updatePortalOptions();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
152
283
|
}
|
|
153
284
|
|
|
154
285
|
public handleFocus() {
|
|
@@ -156,6 +287,10 @@ export class NileAutoComplete extends NileElement {
|
|
|
156
287
|
return;
|
|
157
288
|
}
|
|
158
289
|
|
|
290
|
+
if(this.portal) {
|
|
291
|
+
this.inputElement?.focus();
|
|
292
|
+
}
|
|
293
|
+
|
|
159
294
|
// Delay opening the dropdown to allow focus to take effect
|
|
160
295
|
setTimeout(() => {
|
|
161
296
|
this.isDropdownOpen = true;
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import {
|
|
2
|
+
autoUpdate,
|
|
3
|
+
computePosition,
|
|
4
|
+
flip,
|
|
5
|
+
offset,
|
|
6
|
+
shift,
|
|
7
|
+
size,
|
|
8
|
+
platform,
|
|
9
|
+
type Placement,
|
|
10
|
+
type MiddlewareData,
|
|
11
|
+
type ComputePositionConfig
|
|
12
|
+
} from '@floating-ui/dom';
|
|
13
|
+
import { PortalUtils, PortalContentUtils, PortalEventUtils } from './portal-utils';
|
|
14
|
+
|
|
15
|
+
export class AutoCompletePortalManager {
|
|
16
|
+
private portalContainer: HTMLElement | null = null;
|
|
17
|
+
private originalMenuParent: HTMLElement | null = null;
|
|
18
|
+
private measuredMenuHeight: number | null = null;
|
|
19
|
+
private component: any;
|
|
20
|
+
private clonedMenu: HTMLElement | null = null;
|
|
21
|
+
private cleanupAutoUpdate: (() => void) | null = null;
|
|
22
|
+
private currentPlacement: Placement = 'bottom';
|
|
23
|
+
private currentMiddlewareData: MiddlewareData | null = null;
|
|
24
|
+
|
|
25
|
+
constructor(component: any) {
|
|
26
|
+
this.component = component;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private createPortalAppendContainer(): HTMLElement {
|
|
30
|
+
const container = document.createElement('div');
|
|
31
|
+
container.style.position = 'absolute';
|
|
32
|
+
container.style.zIndex = '9999';
|
|
33
|
+
container.style.pointerEvents = 'none';
|
|
34
|
+
container.style.width = 'auto';
|
|
35
|
+
container.style.minWidth = 'auto';
|
|
36
|
+
container.className = 'nile-auto-complete-portal-append';
|
|
37
|
+
return container;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
positionPortalAppend(): void {
|
|
41
|
+
if (!this.portalContainer || !this.component.dropdownElement) return;
|
|
42
|
+
|
|
43
|
+
this.measureMenuHeight();
|
|
44
|
+
this.computeFloatingUIPosition();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private measureMenuHeight(): void {
|
|
48
|
+
if (this.measuredMenuHeight || !this.portalContainer) return;
|
|
49
|
+
|
|
50
|
+
this.portalContainer.style.position = 'absolute';
|
|
51
|
+
this.portalContainer.style.visibility = 'hidden';
|
|
52
|
+
this.portalContainer.style.top = '0px';
|
|
53
|
+
this.portalContainer.style.left = '0px';
|
|
54
|
+
|
|
55
|
+
this.portalContainer.offsetHeight;
|
|
56
|
+
|
|
57
|
+
this.measuredMenuHeight = this.portalContainer.offsetHeight;
|
|
58
|
+
|
|
59
|
+
this.portalContainer.style.visibility = '';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private async computeFloatingUIPosition(): Promise<void> {
|
|
63
|
+
if (!this.portalContainer) return;
|
|
64
|
+
|
|
65
|
+
const referenceElement = this.component.inputElement || this.component;
|
|
66
|
+
const floatingElement = this.portalContainer;
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const { x, y, placement, middlewareData } = await this.calculateFloatingUIPosition(
|
|
70
|
+
referenceElement,
|
|
71
|
+
floatingElement
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
this.applyFloatingUIPosition(floatingElement, referenceElement, x, y, placement, middlewareData);
|
|
75
|
+
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.warn('Floating UI positioning failed, falling back to simple positioning:', error);
|
|
78
|
+
this.fallbackPositioning();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private async calculateFloatingUIPosition(
|
|
83
|
+
referenceElement: HTMLElement,
|
|
84
|
+
floatingElement: HTMLElement
|
|
85
|
+
): Promise<{ x: number; y: number; placement: Placement; middlewareData: MiddlewareData }> {
|
|
86
|
+
const boundary = PortalUtils.findBoundaryElements(referenceElement);
|
|
87
|
+
// Use 'bottom-start' or 'top-start' to align left edges for auto-width menu
|
|
88
|
+
const basePlacement = PortalUtils.getOptimalPlacement(referenceElement);
|
|
89
|
+
const initialPlacement = basePlacement === 'top' ? 'top-start' : 'bottom-start';
|
|
90
|
+
const middleware = this.createFloatingUIMiddleware(boundary);
|
|
91
|
+
|
|
92
|
+
return await computePosition(referenceElement, floatingElement, {
|
|
93
|
+
placement: initialPlacement,
|
|
94
|
+
strategy: 'fixed',
|
|
95
|
+
middleware,
|
|
96
|
+
platform: this.createCustomPlatform()
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private createFloatingUIMiddleware(boundary: Element[] | undefined): ComputePositionConfig['middleware'] {
|
|
101
|
+
return [
|
|
102
|
+
offset(4),
|
|
103
|
+
size({
|
|
104
|
+
apply: this.handleSizeMiddleware.bind(this),
|
|
105
|
+
padding: 10,
|
|
106
|
+
boundary: boundary
|
|
107
|
+
}),
|
|
108
|
+
flip({
|
|
109
|
+
fallbackPlacements: ['bottom-start', 'top-start', 'bottom', 'top', 'bottom-end', 'top-end'],
|
|
110
|
+
fallbackStrategy: 'bestFit',
|
|
111
|
+
padding: 10,
|
|
112
|
+
boundary: boundary
|
|
113
|
+
}),
|
|
114
|
+
shift({
|
|
115
|
+
padding: 10,
|
|
116
|
+
crossAxis: true,
|
|
117
|
+
boundary: boundary
|
|
118
|
+
})
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private handleSizeMiddleware({ availableWidth, availableHeight, elements, rects }: {
|
|
123
|
+
availableWidth: number;
|
|
124
|
+
availableHeight: number;
|
|
125
|
+
elements: { floating: HTMLElement };
|
|
126
|
+
rects: { reference: { x: number; y: number; width: number; height: number } };
|
|
127
|
+
}): void {
|
|
128
|
+
const maxHeight = PortalUtils.calculateOptimalHeight(
|
|
129
|
+
rects.reference,
|
|
130
|
+
window.innerHeight,
|
|
131
|
+
this.currentPlacement
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// elements.floating.style.maxWidth = `${availableWidth}px`;
|
|
135
|
+
elements.floating.style.maxHeight = `${maxHeight}px`;
|
|
136
|
+
|
|
137
|
+
elements.floating.style.setProperty('--auto-size-available-width', `${availableWidth}px`);
|
|
138
|
+
elements.floating.style.setProperty('--auto-size-available-height', `${maxHeight}px`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private createCustomPlatform() {
|
|
142
|
+
return platform;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private applyFloatingUIPosition(
|
|
146
|
+
floatingElement: HTMLElement,
|
|
147
|
+
referenceElement: HTMLElement,
|
|
148
|
+
x: number,
|
|
149
|
+
y: number,
|
|
150
|
+
placement: Placement,
|
|
151
|
+
middlewareData: MiddlewareData
|
|
152
|
+
): void {
|
|
153
|
+
// For auto-complete, align left edge with input element
|
|
154
|
+
// Use reference element's left position, not Floating UI's x (which might shift)
|
|
155
|
+
const referenceRect = referenceElement.getBoundingClientRect();
|
|
156
|
+
|
|
157
|
+
Object.assign(floatingElement.style, {
|
|
158
|
+
left: `${referenceRect.left}px`,
|
|
159
|
+
top: `${y}px`,
|
|
160
|
+
position: 'fixed',
|
|
161
|
+
pointerEvents: 'auto',
|
|
162
|
+
width: 'auto',
|
|
163
|
+
minWidth: 'auto'
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
this.currentPlacement = placement;
|
|
167
|
+
this.currentMiddlewareData = middlewareData;
|
|
168
|
+
|
|
169
|
+
PortalUtils.applyCollisionData(floatingElement, middlewareData, placement);
|
|
170
|
+
|
|
171
|
+
const placementClass = placement.split('-')[0];
|
|
172
|
+
floatingElement.className = `nile-auto-complete-portal-append menu__listbox--${placementClass}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private fallbackPositioning(): void {
|
|
176
|
+
if (!this.portalContainer) return;
|
|
177
|
+
|
|
178
|
+
const referenceElement = this.component.inputElement || this.component;
|
|
179
|
+
const rect = referenceElement.getBoundingClientRect();
|
|
180
|
+
const viewportHeight = window.innerHeight;
|
|
181
|
+
const menuHeight = this.measuredMenuHeight || 200;
|
|
182
|
+
|
|
183
|
+
const spaceBelow = viewportHeight - rect.bottom;
|
|
184
|
+
const spaceAbove = rect.top;
|
|
185
|
+
|
|
186
|
+
let topPosition: number;
|
|
187
|
+
let placementClass: string;
|
|
188
|
+
let maxHeight: number;
|
|
189
|
+
|
|
190
|
+
if (spaceAbove > spaceBelow) {
|
|
191
|
+
maxHeight = Math.max(spaceAbove - 20, 100);
|
|
192
|
+
topPosition = Math.max(rect.top - maxHeight - 4, 10);
|
|
193
|
+
placementClass = 'top';
|
|
194
|
+
} else {
|
|
195
|
+
maxHeight = Math.max(spaceBelow - 20, 100);
|
|
196
|
+
topPosition = rect.bottom + 4;
|
|
197
|
+
placementClass = 'bottom';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
this.portalContainer.style.left = `${rect.left}px`;
|
|
201
|
+
this.portalContainer.style.top = `${topPosition}px`;
|
|
202
|
+
// Let the menu auto-size based on its content, don't force width to match input
|
|
203
|
+
this.portalContainer.style.width = 'auto';
|
|
204
|
+
this.portalContainer.style.minWidth = 'auto';
|
|
205
|
+
this.portalContainer.style.maxHeight = `${maxHeight}px`;
|
|
206
|
+
this.portalContainer.style.pointerEvents = 'auto';
|
|
207
|
+
this.portalContainer.className = `nile-auto-complete-portal-append menu__listbox--${placementClass}`;
|
|
208
|
+
|
|
209
|
+
this.calculateAndSetAutoSizeProperties(rect, topPosition, placementClass);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private calculateAndSetAutoSizeProperties(rect: DOMRect, topPosition: number, placementClass: string): void {
|
|
213
|
+
if (!this.portalContainer) return;
|
|
214
|
+
|
|
215
|
+
const viewportHeight = window.innerHeight;
|
|
216
|
+
const viewportWidth = window.innerWidth;
|
|
217
|
+
|
|
218
|
+
let availableHeight: number;
|
|
219
|
+
if (placementClass === 'top') {
|
|
220
|
+
availableHeight = rect.top - 10;
|
|
221
|
+
} else {
|
|
222
|
+
availableHeight = viewportHeight - rect.bottom - 10;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const availableWidth = Math.min(rect.width, viewportWidth - rect.left - 10);
|
|
226
|
+
|
|
227
|
+
this.portalContainer.style.setProperty('--auto-size-available-height', `${Math.max(availableHeight, 100)}px`);
|
|
228
|
+
this.portalContainer.style.setProperty('--auto-size-available-width', `${Math.max(availableWidth, 200)}px`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
updatePortalAppendPosition(): void {
|
|
232
|
+
if (this.component.portal && this.portalContainer) {
|
|
233
|
+
this.positionPortalAppend();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
handleWindowResize(): void {
|
|
238
|
+
if (this.component.portal && this.portalContainer) {
|
|
239
|
+
this.positionPortalAppend();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private setupAutoUpdatePositioning(): void {
|
|
244
|
+
if (!this.portalContainer || !this.component) return;
|
|
245
|
+
|
|
246
|
+
this.cleanupAutoUpdatePositioning();
|
|
247
|
+
|
|
248
|
+
this.cleanupAutoUpdate = autoUpdate(
|
|
249
|
+
this.component,
|
|
250
|
+
this.portalContainer,
|
|
251
|
+
() => {
|
|
252
|
+
this.computeFloatingUIPosition();
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
ancestorScroll: true,
|
|
256
|
+
ancestorResize: true,
|
|
257
|
+
elementResize: true,
|
|
258
|
+
layoutShift: true,
|
|
259
|
+
animationFrame: true
|
|
260
|
+
}
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private cleanupAutoUpdatePositioning(): void {
|
|
265
|
+
if (this.cleanupAutoUpdate) {
|
|
266
|
+
this.cleanupAutoUpdate();
|
|
267
|
+
this.cleanupAutoUpdate = null;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private injectStylesToDocument(): void {
|
|
272
|
+
if (!this.portalContainer) return;
|
|
273
|
+
|
|
274
|
+
const styleId = PortalUtils.generateStyleId();
|
|
275
|
+
|
|
276
|
+
if (document.getElementById(styleId)) return;
|
|
277
|
+
|
|
278
|
+
const componentStyles = (this.component.constructor as any).styles;
|
|
279
|
+
if (!componentStyles) return;
|
|
280
|
+
|
|
281
|
+
const styleElement = document.createElement('style');
|
|
282
|
+
styleElement.id = styleId;
|
|
283
|
+
styleElement.textContent = PortalUtils.extractStylesAsCSS(componentStyles);
|
|
284
|
+
|
|
285
|
+
document.head.appendChild(styleElement);
|
|
286
|
+
|
|
287
|
+
(this.portalContainer as any).__injectedStyleId = styleId;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private adoptStylesToPortalAppend(): void {
|
|
291
|
+
if (!this.portalContainer) return;
|
|
292
|
+
this.injectStylesToDocument();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
setupPortalAppend(): void {
|
|
296
|
+
if (!this.component.portal) return;
|
|
297
|
+
|
|
298
|
+
this.component.updateComplete.then(() => {
|
|
299
|
+
setTimeout(() => {
|
|
300
|
+
// Try to find menu in shadow root first, then fallback to querySelector
|
|
301
|
+
const menu = this.component.shadowRoot?.querySelector('#content-menu') as HTMLElement ||
|
|
302
|
+
this.component.querySelector('#content-menu') as HTMLElement;
|
|
303
|
+
if (menu && this.component.isDropdownOpen) {
|
|
304
|
+
this.originalMenuParent = menu.parentElement as HTMLElement;
|
|
305
|
+
|
|
306
|
+
this.clonedMenu = this.createPortalMenu();
|
|
307
|
+
|
|
308
|
+
this.portalContainer = this.createPortalAppendContainer();
|
|
309
|
+
this.portalContainer.appendChild(this.clonedMenu);
|
|
310
|
+
document.body.appendChild(this.portalContainer);
|
|
311
|
+
|
|
312
|
+
this.adoptStylesToPortalAppend();
|
|
313
|
+
|
|
314
|
+
this.clonedMenu.style.display = '';
|
|
315
|
+
this.positionPortalAppend();
|
|
316
|
+
|
|
317
|
+
this.setupPortalEventListeners();
|
|
318
|
+
|
|
319
|
+
this.setupAutoUpdatePositioning();
|
|
320
|
+
|
|
321
|
+
window.addEventListener('resize', this.handleWindowResize.bind(this));
|
|
322
|
+
}
|
|
323
|
+
}, 10);
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private createPortalMenu(): HTMLElement {
|
|
328
|
+
return PortalContentUtils.createPortalMenu(this.component);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private setupPortalEventListeners(): void {
|
|
332
|
+
PortalEventUtils.setupPortalEventListeners(this.clonedMenu!, this.component);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
cleanupPortalAppend(): void {
|
|
336
|
+
this.cleanupAutoUpdatePositioning();
|
|
337
|
+
|
|
338
|
+
if (this.portalContainer && this.portalContainer.parentNode) {
|
|
339
|
+
const injectedStyleId = (this.portalContainer as any).__injectedStyleId;
|
|
340
|
+
if (injectedStyleId) {
|
|
341
|
+
const styleElement = document.getElementById(injectedStyleId);
|
|
342
|
+
if (styleElement) {
|
|
343
|
+
styleElement.remove();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
this.portalContainer.parentNode.removeChild(this.portalContainer);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
window.removeEventListener('resize', this.handleWindowResize.bind(this));
|
|
351
|
+
|
|
352
|
+
this.portalContainer = null;
|
|
353
|
+
this.originalMenuParent = null;
|
|
354
|
+
this.clonedMenu = null;
|
|
355
|
+
this.measuredMenuHeight = null;
|
|
356
|
+
this.currentPlacement = 'bottom';
|
|
357
|
+
this.currentMiddlewareData = null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
get portalContainerElement(): HTMLElement | null {
|
|
361
|
+
return this.portalContainer;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
resetMeasuredHeight(): void {
|
|
365
|
+
this.measuredMenuHeight = null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
updatePortalOptions(): void {
|
|
369
|
+
if (this.portalContainer && this.clonedMenu) {
|
|
370
|
+
PortalContentUtils.updatePortalMenuItems(this.clonedMenu, this.component);
|
|
371
|
+
this.forceReposition();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
forceReposition(): void {
|
|
376
|
+
if (this.portalContainer) {
|
|
377
|
+
this.computeFloatingUIPosition();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
getCurrentPlacement(): Placement {
|
|
382
|
+
return this.currentPlacement;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
getCurrentMiddlewareData(): MiddlewareData | null {
|
|
386
|
+
return this.currentMiddlewareData;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
isUsingFloatingUI(): boolean {
|
|
390
|
+
return this.cleanupAutoUpdate !== null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
isPositioningOptimal(): boolean {
|
|
394
|
+
if (!this.portalContainer || !this.currentMiddlewareData) return true;
|
|
395
|
+
|
|
396
|
+
const referenceElement = this.component.inputElement || this.component;
|
|
397
|
+
const rect = referenceElement.getBoundingClientRect();
|
|
398
|
+
const viewportHeight = window.innerHeight;
|
|
399
|
+
const spaceBelow = viewportHeight - rect.bottom;
|
|
400
|
+
const spaceAbove = rect.top;
|
|
401
|
+
|
|
402
|
+
const isAbove = this.currentPlacement.startsWith('top');
|
|
403
|
+
const isBelow = this.currentPlacement.startsWith('bottom');
|
|
404
|
+
|
|
405
|
+
if (isAbove && spaceBelow > spaceAbove) return false;
|
|
406
|
+
if (isBelow && spaceAbove > spaceBelow) return false;
|
|
407
|
+
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
}
|