@aquera/nile-elements 1.6.0 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.esm.js +1 -1
- package/dist/index.js +301 -185
- package/dist/nile-floating-panel/index.cjs.js +1 -1
- package/dist/nile-floating-panel/index.esm.js +1 -1
- package/dist/nile-floating-panel/nile-floating-panel.cjs.js +1 -1
- package/dist/nile-floating-panel/nile-floating-panel.cjs.js.map +1 -1
- package/dist/nile-floating-panel/nile-floating-panel.css.cjs.js +1 -1
- package/dist/nile-floating-panel/nile-floating-panel.css.cjs.js.map +1 -1
- package/dist/nile-floating-panel/nile-floating-panel.css.esm.js +137 -21
- package/dist/nile-floating-panel/nile-floating-panel.esm.js +1 -1
- package/dist/nile-lite-tooltip/index.cjs.js +1 -1
- package/dist/nile-lite-tooltip/index.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/index.d.ts +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/nile-floating-panel/index.js.map +1 -1
- package/dist/src/nile-floating-panel/nile-floating-panel.css.d.ts +1 -1
- package/dist/src/nile-floating-panel/nile-floating-panel.css.js +147 -20
- package/dist/src/nile-floating-panel/nile-floating-panel.css.js.map +1 -1
- package/dist/src/nile-floating-panel/nile-floating-panel.d.ts +90 -24
- package/dist/src/nile-floating-panel/nile-floating-panel.js +478 -159
- package/dist/src/nile-floating-panel/nile-floating-panel.js.map +1 -1
- package/dist/src/version.js +1 -1
- package/dist/src/version.js.map +1 -1
- package/dist/tippy.esm-57628c2b.esm.js +1 -0
- package/dist/tippy.esm-78baa8f2.cjs.js +2 -0
- package/dist/tippy.esm-78baa8f2.cjs.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -3
- package/src/index.ts +2 -2
- package/src/nile-floating-panel/index.ts +0 -1
- package/src/nile-floating-panel/nile-floating-panel.css.ts +149 -21
- package/src/nile-floating-panel/nile-floating-panel.ts +489 -190
- package/vscode-html-custom-data.json +213 -23
- package/dist/nile-floating-panel/anchor-manager.cjs.js +0 -2
- package/dist/nile-floating-panel/anchor-manager.cjs.js.map +0 -1
- package/dist/nile-floating-panel/anchor-manager.esm.js +0 -1
- package/dist/nile-floating-panel/content-manager.cjs.js +0 -2
- package/dist/nile-floating-panel/content-manager.cjs.js.map +0 -1
- package/dist/nile-floating-panel/content-manager.esm.js +0 -1
- package/dist/nile-floating-panel/event-manager.cjs.js +0 -2
- package/dist/nile-floating-panel/event-manager.cjs.js.map +0 -1
- package/dist/nile-floating-panel/event-manager.esm.js +0 -1
- package/dist/nile-floating-panel/position-manager.cjs.js +0 -2
- package/dist/nile-floating-panel/position-manager.cjs.js.map +0 -1
- package/dist/nile-floating-panel/position-manager.esm.js +0 -1
- package/dist/nile-floating-panel/style-manager.cjs.js +0 -2
- package/dist/nile-floating-panel/style-manager.cjs.js.map +0 -1
- package/dist/nile-floating-panel/style-manager.esm.js +0 -1
- package/dist/nile-floating-panel/types.cjs.js +0 -2
- package/dist/nile-floating-panel/types.cjs.js.map +0 -1
- package/dist/nile-floating-panel/types.esm.js +0 -1
- package/dist/src/nile-floating-panel/anchor-manager.d.ts +0 -6
- package/dist/src/nile-floating-panel/anchor-manager.js +0 -27
- package/dist/src/nile-floating-panel/anchor-manager.js.map +0 -1
- package/dist/src/nile-floating-panel/content-manager.d.ts +0 -5
- package/dist/src/nile-floating-panel/content-manager.js +0 -44
- package/dist/src/nile-floating-panel/content-manager.js.map +0 -1
- package/dist/src/nile-floating-panel/event-manager.d.ts +0 -14
- package/dist/src/nile-floating-panel/event-manager.js +0 -52
- package/dist/src/nile-floating-panel/event-manager.js.map +0 -1
- package/dist/src/nile-floating-panel/position-manager.d.ts +0 -17
- package/dist/src/nile-floating-panel/position-manager.js +0 -72
- package/dist/src/nile-floating-panel/position-manager.js.map +0 -1
- package/dist/src/nile-floating-panel/style-manager.d.ts +0 -9
- package/dist/src/nile-floating-panel/style-manager.js +0 -44
- package/dist/src/nile-floating-panel/style-manager.js.map +0 -1
- package/dist/src/nile-floating-panel/types.d.ts +0 -11
- package/dist/src/nile-floating-panel/types.js +0 -2
- package/dist/src/nile-floating-panel/types.js.map +0 -1
- package/src/nile-floating-panel/anchor-manager.ts +0 -33
- package/src/nile-floating-panel/content-manager.ts +0 -54
- package/src/nile-floating-panel/event-manager.ts +0 -74
- package/src/nile-floating-panel/position-manager.ts +0 -102
- package/src/nile-floating-panel/style-manager.ts +0 -54
- package/src/nile-floating-panel/types.ts +0 -15
|
@@ -1,285 +1,584 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Copyright Aquera Inc 2025
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the BSD-3-Clause license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { CSSResultArray, PropertyValues } from 'lit';
|
|
8
9
|
import { customElement, property } from 'lit/decorators.js';
|
|
9
10
|
import { styles } from './nile-floating-panel.css';
|
|
10
11
|
import NileElement from '../internal/nile-element';
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
import
|
|
12
|
+
import tippy, {
|
|
13
|
+
Instance,
|
|
14
|
+
Props,
|
|
15
|
+
roundArrow,
|
|
16
|
+
followCursor as followCursorPlugin,
|
|
17
|
+
} from 'tippy.js';
|
|
18
|
+
import {
|
|
19
|
+
parseFollowCursor,
|
|
20
|
+
parseDuration,
|
|
21
|
+
} from '../nile-lite-tooltip/utils';
|
|
22
|
+
import { VisibilityManager } from '../utilities/visibility-manager.js';
|
|
18
23
|
|
|
19
24
|
/**
|
|
20
|
-
* Nile floating
|
|
25
|
+
* Nile floating-panel component.
|
|
26
|
+
*
|
|
27
|
+
* A popover that supports rich content (title, body, actions).
|
|
28
|
+
*
|
|
29
|
+
* **Wrapper mode** (default): first child element is the trigger.
|
|
30
|
+
* **For mode**: set `for="elementId"` to attach to an external element.
|
|
21
31
|
*
|
|
22
32
|
* @tag nile-floating-panel
|
|
23
|
-
*
|
|
24
|
-
* @
|
|
33
|
+
*
|
|
34
|
+
* @fires nile-init - Component initialized.
|
|
35
|
+
* @fires nile-destroy - Component destroyed.
|
|
36
|
+
* @fires nile-show - Panel opened.
|
|
37
|
+
* @fires nile-hide - Panel closed.
|
|
38
|
+
* @fires nile-after-show - Panel fully visible after animation.
|
|
39
|
+
* @fires nile-after-hide - Panel fully hidden after animation.
|
|
40
|
+
* @fires nile-toggle - Open/close transition (detail.open).
|
|
41
|
+
* @fires nile-visibility-change - Hidden by scroll/tab change.
|
|
25
42
|
*/
|
|
26
43
|
@customElement('nile-floating-panel')
|
|
27
44
|
export class NileFloatingPanel extends NileElement {
|
|
45
|
+
private static _groups = new Map<string, Set<NileFloatingPanel>>();
|
|
46
|
+
|
|
47
|
+
private static _reducedMotionQuery: MediaQueryList | null = null;
|
|
48
|
+
|
|
49
|
+
private static get prefersReducedMotion(): boolean {
|
|
50
|
+
if (!NileFloatingPanel._reducedMotionQuery) {
|
|
51
|
+
NileFloatingPanel._reducedMotionQuery =
|
|
52
|
+
window.matchMedia('(prefers-reduced-motion: reduce)');
|
|
53
|
+
}
|
|
54
|
+
return NileFloatingPanel._reducedMotionQuery.matches;
|
|
55
|
+
}
|
|
56
|
+
|
|
28
57
|
public static get styles(): CSSResultArray {
|
|
29
58
|
return [styles];
|
|
30
59
|
}
|
|
31
60
|
|
|
32
|
-
|
|
61
|
+
protected createRenderRoot() {
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── Tippy.js props ───
|
|
66
|
+
|
|
67
|
+
@property({ type: String })
|
|
68
|
+
placement:
|
|
69
|
+
| 'top'
|
|
70
|
+
| 'top-start'
|
|
71
|
+
| 'top-end'
|
|
72
|
+
| 'right'
|
|
73
|
+
| 'right-start'
|
|
74
|
+
| 'right-end'
|
|
75
|
+
| 'bottom'
|
|
76
|
+
| 'bottom-start'
|
|
77
|
+
| 'bottom-end'
|
|
78
|
+
| 'left'
|
|
79
|
+
| 'left-start'
|
|
80
|
+
| 'left-end'
|
|
81
|
+
| 'auto'
|
|
82
|
+
| 'auto-start'
|
|
83
|
+
| 'auto-end' = 'bottom';
|
|
84
|
+
|
|
85
|
+
@property({ type: String }) trigger: string = 'click';
|
|
86
|
+
|
|
87
|
+
@property({ type: Number }) distance = 12;
|
|
88
|
+
|
|
89
|
+
@property({ type: Number }) skidding = 0;
|
|
90
|
+
|
|
91
|
+
@property({ type: String, reflect: true })
|
|
92
|
+
arrow: 'round' | 'default' | 'none' = 'round';
|
|
93
|
+
|
|
94
|
+
@property({ type: String, reflect: true }) animation: string = 'fade';
|
|
95
|
+
|
|
96
|
+
@property({ type: String, reflect: true }) duration:
|
|
97
|
+
| string
|
|
98
|
+
| number
|
|
99
|
+
| [number, number] = 200;
|
|
100
|
+
|
|
101
|
+
@property({ type: String, reflect: true }) delay:
|
|
102
|
+
| number
|
|
103
|
+
| [number, number] = 0;
|
|
104
|
+
|
|
105
|
+
@property({ type: Boolean, reflect: true }) interactive = true;
|
|
106
|
+
|
|
107
|
+
@property({ type: Number, reflect: true }) interactiveBorder = 2;
|
|
108
|
+
|
|
109
|
+
@property({ type: String, reflect: true }) maxWidth: string | number = 'none';
|
|
110
|
+
|
|
111
|
+
@property({ type: Number, reflect: true }) zIndex = 9999;
|
|
112
|
+
|
|
113
|
+
@property({ type: String, reflect: true })
|
|
114
|
+
followCursor:
|
|
115
|
+
| boolean
|
|
116
|
+
| 'initial'
|
|
117
|
+
| 'horizontal'
|
|
118
|
+
| 'vertical'
|
|
119
|
+
| 'true'
|
|
120
|
+
| 'false' = false;
|
|
121
|
+
|
|
122
|
+
@property({ type: Boolean, reflect: true }) hideOnClick:
|
|
123
|
+
| boolean
|
|
124
|
+
| 'toggle' = true;
|
|
125
|
+
|
|
126
|
+
@property({ type: Boolean, reflect: true }) inertia = false;
|
|
127
|
+
|
|
128
|
+
@property({ type: Boolean, reflect: true }) allowHTML = false;
|
|
129
|
+
|
|
130
|
+
@property({ type: Boolean, reflect: true }) flip = true;
|
|
131
|
+
|
|
132
|
+
// ─── Popover-like props ───
|
|
133
|
+
|
|
134
|
+
@property({ type: String, attribute: 'for' }) for: string | null = null;
|
|
135
|
+
|
|
136
|
+
@property({ type: Boolean, reflect: true }) open = false;
|
|
137
|
+
|
|
138
|
+
@property({ type: Boolean, reflect: true }) preventOverlayClose = false;
|
|
139
|
+
|
|
140
|
+
@property({ type: String, reflect: true }) title = '';
|
|
33
141
|
|
|
34
|
-
@property(
|
|
142
|
+
@property({ type: Boolean, reflect: true }) disabled = false;
|
|
35
143
|
|
|
36
|
-
@property({ type:
|
|
144
|
+
@property({ type: String, reflect: true }) width?: string;
|
|
37
145
|
|
|
38
|
-
@property({
|
|
39
|
-
type: Boolean,
|
|
40
|
-
reflect: true,
|
|
41
|
-
attribute: 'close-on-outside-click',
|
|
42
|
-
converter: {
|
|
43
|
-
fromAttribute: (value: string | null) => (!value || value === 'false' ? false : true),
|
|
44
|
-
toAttribute: (value: boolean) => (value ? 'true' : 'false'),
|
|
45
|
-
},
|
|
46
|
-
})
|
|
47
|
-
closeOnOutsideClick = true;
|
|
146
|
+
@property({ type: String, reflect: true }) height?: string;
|
|
48
147
|
|
|
49
|
-
|
|
148
|
+
/** When set, only one panel in the same group can be open at a time. */
|
|
149
|
+
@property({ type: String, reflect: true }) group: string | null = null;
|
|
150
|
+
|
|
151
|
+
/** Close the panel when Escape is pressed. */
|
|
152
|
+
@property({ type: Boolean, reflect: true }) closeOnEscape = true;
|
|
153
|
+
|
|
154
|
+
// ─── Visibility manager props ───
|
|
155
|
+
|
|
156
|
+
@property({ type: Boolean, reflect: true }) enableVisibilityEffect = false;
|
|
50
157
|
|
|
51
158
|
@property({ type: Boolean, reflect: true }) enableTabClose = false;
|
|
52
159
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
private
|
|
56
|
-
private eventManager: EventManager = new EventManager(this);
|
|
160
|
+
// ─── Internal state ───
|
|
161
|
+
|
|
162
|
+
private tippyInstance: Instance | null = null;
|
|
57
163
|
private visibilityManager?: VisibilityManager;
|
|
164
|
+
private panelContainer: HTMLElement | null = null;
|
|
165
|
+
private anchorEl: HTMLElement | null = null;
|
|
166
|
+
private _suppressOpenWatch = false;
|
|
167
|
+
private _panelId = `nile-fp-${Math.random().toString(36).slice(2, 9)}`;
|
|
168
|
+
private _boundEscHandler = this._handleEscapeKey.bind(this);
|
|
169
|
+
private _pendingShowListener: (() => void) | null = null;
|
|
170
|
+
private _pendingHideListener: (() => void) | null = null;
|
|
171
|
+
|
|
172
|
+
// ─── Lifecycle ───
|
|
173
|
+
|
|
174
|
+
protected firstUpdated(): void {
|
|
175
|
+
this._buildDOM();
|
|
176
|
+
this._attachTippy();
|
|
177
|
+
this._joinGroup();
|
|
58
178
|
|
|
59
|
-
|
|
60
|
-
|
|
179
|
+
this.visibilityManager = new VisibilityManager({
|
|
180
|
+
host: this,
|
|
181
|
+
target: this.anchorEl || null,
|
|
182
|
+
enableVisibilityEffect: this.enableVisibilityEffect,
|
|
183
|
+
enableTabClose: this.enableTabClose,
|
|
184
|
+
isOpen: () => this.open,
|
|
185
|
+
onAnchorOutOfView: () => {
|
|
186
|
+
this._setOpen(false);
|
|
187
|
+
this.tippyInstance?.hide();
|
|
188
|
+
this.emit('nile-visibility-change', {
|
|
189
|
+
visible: false,
|
|
190
|
+
reason: 'anchor-out-of-view',
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
onDocumentHidden: () => {
|
|
194
|
+
this._setOpen(false);
|
|
195
|
+
this.tippyInstance?.hide();
|
|
196
|
+
this.emit('nile-visibility-change', {
|
|
197
|
+
visible: false,
|
|
198
|
+
reason: 'document-hidden',
|
|
199
|
+
});
|
|
200
|
+
},
|
|
201
|
+
emit: (event, detail) => this.emit(`nile-${event}`, detail),
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
this.emit('nile-init');
|
|
61
205
|
}
|
|
62
206
|
|
|
63
|
-
disconnectedCallback() {
|
|
207
|
+
disconnectedCallback(): void {
|
|
64
208
|
super.disconnectedCallback();
|
|
65
|
-
this.
|
|
209
|
+
this._cleanupPendingShowListener();
|
|
210
|
+
this._cleanupPendingHideListener();
|
|
66
211
|
this.visibilityManager?.cleanup();
|
|
212
|
+
this._leaveGroup();
|
|
213
|
+
this._removeEscListener();
|
|
214
|
+
this._destroyTippy();
|
|
215
|
+
this.emit('nile-destroy');
|
|
67
216
|
}
|
|
68
217
|
|
|
69
|
-
updated(
|
|
70
|
-
super.updated(
|
|
218
|
+
updated(changed: PropertyValues): void {
|
|
219
|
+
super.updated(changed);
|
|
71
220
|
|
|
72
|
-
if (
|
|
221
|
+
if (!this.panelContainer) return;
|
|
222
|
+
|
|
223
|
+
if (changed.has('open') && !this._suppressOpenWatch) {
|
|
73
224
|
if (this.open) {
|
|
74
|
-
this.emit('nile-show');
|
|
75
|
-
this.setupPanel();
|
|
76
225
|
this.visibilityManager?.setup();
|
|
226
|
+
queueMicrotask(() => this.tippyInstance?.show());
|
|
77
227
|
} else {
|
|
78
|
-
this.emit('nile-hide');
|
|
79
228
|
this.visibilityManager?.cleanup();
|
|
80
|
-
this.
|
|
229
|
+
this.tippyInstance?.hide();
|
|
81
230
|
}
|
|
82
231
|
}
|
|
83
232
|
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
this.
|
|
87
|
-
this.panelContainer
|
|
88
|
-
) {
|
|
89
|
-
this.eventManager.updateOutsideClickHandler(
|
|
90
|
-
this.panelContainer,
|
|
91
|
-
this.closeOnOutsideClick,
|
|
92
|
-
this.open
|
|
93
|
-
);
|
|
233
|
+
if (changed.has('group')) {
|
|
234
|
+
this._leaveGroup(changed.get('group') as string | null);
|
|
235
|
+
this._joinGroup();
|
|
94
236
|
}
|
|
95
237
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
238
|
+
const rebuildProps: string[] = [
|
|
239
|
+
'placement', 'trigger', 'distance', 'skidding', 'arrow',
|
|
240
|
+
'animation', 'duration', 'delay', 'interactive', 'interactiveBorder',
|
|
241
|
+
'maxWidth', 'zIndex', 'followCursor', 'hideOnClick', 'inertia',
|
|
242
|
+
'allowHTML', 'flip', 'preventOverlayClose', 'disabled', 'width', 'height',
|
|
243
|
+
];
|
|
100
244
|
|
|
101
|
-
if (
|
|
102
|
-
|
|
103
|
-
changedProperties.has('enableTabClose')) &&
|
|
104
|
-
this.open
|
|
105
|
-
) {
|
|
106
|
-
this.setupVisibilityManager();
|
|
245
|
+
if (rebuildProps.some(p => changed.has(p))) {
|
|
246
|
+
this._attachTippy();
|
|
107
247
|
}
|
|
248
|
+
}
|
|
108
249
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
250
|
+
// ─── Public API ───
|
|
251
|
+
|
|
252
|
+
/** Programmatically shows the panel. Returns a promise that resolves after the show animation. */
|
|
253
|
+
public show(): Promise<void> {
|
|
254
|
+
this.open = true;
|
|
255
|
+
return new Promise<void>(resolve => {
|
|
256
|
+
this._cleanupPendingShowListener();
|
|
257
|
+
const handler = () => {
|
|
258
|
+
this._pendingShowListener = null;
|
|
259
|
+
resolve();
|
|
260
|
+
};
|
|
261
|
+
this._pendingShowListener = handler;
|
|
262
|
+
this.addEventListener('nile-after-show', handler, { once: true });
|
|
263
|
+
});
|
|
264
|
+
}
|
|
116
265
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
266
|
+
/** Programmatically hides the panel. Returns a promise that resolves after the hide animation. */
|
|
267
|
+
public hide(): Promise<void> {
|
|
268
|
+
this.open = false;
|
|
269
|
+
return new Promise<void>(resolve => {
|
|
270
|
+
this._cleanupPendingHideListener();
|
|
271
|
+
const handler = () => {
|
|
272
|
+
this._pendingHideListener = null;
|
|
273
|
+
resolve();
|
|
274
|
+
};
|
|
275
|
+
this._pendingHideListener = handler;
|
|
276
|
+
this.addEventListener('nile-after-hide', handler, { once: true });
|
|
277
|
+
});
|
|
120
278
|
}
|
|
121
279
|
|
|
122
|
-
private
|
|
123
|
-
if (this.
|
|
124
|
-
|
|
280
|
+
private _cleanupPendingShowListener(): void {
|
|
281
|
+
if (this._pendingShowListener) {
|
|
282
|
+
this.removeEventListener('nile-after-show', this._pendingShowListener);
|
|
283
|
+
this._pendingShowListener = null;
|
|
125
284
|
}
|
|
285
|
+
}
|
|
126
286
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
this.
|
|
287
|
+
private _cleanupPendingHideListener(): void {
|
|
288
|
+
if (this._pendingHideListener) {
|
|
289
|
+
this.removeEventListener('nile-after-hide', this._pendingHideListener);
|
|
290
|
+
this._pendingHideListener = null;
|
|
131
291
|
}
|
|
292
|
+
}
|
|
132
293
|
|
|
133
|
-
|
|
134
|
-
this.
|
|
135
|
-
|
|
294
|
+
public toggle(): void {
|
|
295
|
+
this.open = !this.open;
|
|
296
|
+
}
|
|
136
297
|
|
|
137
|
-
|
|
298
|
+
public refresh(): void {
|
|
299
|
+
this._attachTippy();
|
|
300
|
+
}
|
|
138
301
|
|
|
139
|
-
|
|
140
|
-
|
|
302
|
+
/** Returns the current resolved placement from Tippy/Popper. */
|
|
303
|
+
public getCurrentPlacement(): string {
|
|
304
|
+
const popper = this.tippyInstance?.popper;
|
|
305
|
+
const box = popper?.querySelector('.tippy-box') as HTMLElement | null;
|
|
306
|
+
return box?.dataset.placement ?? this.placement;
|
|
307
|
+
}
|
|
141
308
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
this.setupVisibilityManager();
|
|
146
|
-
});
|
|
309
|
+
/** Returns true if the resolved placement matches the requested placement. */
|
|
310
|
+
public isPositioningOptimal(): boolean {
|
|
311
|
+
return this.getCurrentPlacement() === this.placement;
|
|
147
312
|
}
|
|
148
313
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
314
|
+
// ─── Group management ───
|
|
315
|
+
|
|
316
|
+
private _joinGroup(): void {
|
|
317
|
+
if (!this.group) return;
|
|
318
|
+
let set = NileFloatingPanel._groups.get(this.group);
|
|
319
|
+
if (!set) {
|
|
320
|
+
set = new Set();
|
|
321
|
+
NileFloatingPanel._groups.set(this.group, set);
|
|
152
322
|
}
|
|
323
|
+
set.add(this);
|
|
324
|
+
}
|
|
153
325
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
326
|
+
private _leaveGroup(oldGroup?: string | null): void {
|
|
327
|
+
const key = oldGroup ?? this.group;
|
|
328
|
+
if (!key) return;
|
|
329
|
+
const set = NileFloatingPanel._groups.get(key);
|
|
330
|
+
if (set) {
|
|
331
|
+
set.delete(this);
|
|
332
|
+
if (set.size === 0) NileFloatingPanel._groups.delete(key);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private _hideGroupSiblings(): void {
|
|
337
|
+
if (!this.group) return;
|
|
338
|
+
const set = NileFloatingPanel._groups.get(this.group);
|
|
339
|
+
if (!set) return;
|
|
340
|
+
set.forEach(panel => {
|
|
341
|
+
if (panel !== this && panel.open) {
|
|
342
|
+
panel._setOpen(false);
|
|
343
|
+
panel.tippyInstance?.hide();
|
|
160
344
|
}
|
|
161
|
-
);
|
|
345
|
+
});
|
|
162
346
|
}
|
|
163
347
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
348
|
+
// ─── Escape key ───
|
|
349
|
+
|
|
350
|
+
private _addEscListener(): void {
|
|
351
|
+
if (this.closeOnEscape) {
|
|
352
|
+
document.addEventListener('keydown', this._boundEscHandler);
|
|
167
353
|
}
|
|
354
|
+
}
|
|
168
355
|
|
|
169
|
-
|
|
170
|
-
this.
|
|
171
|
-
|
|
172
|
-
this.panelContainer,
|
|
173
|
-
this.position
|
|
174
|
-
);
|
|
356
|
+
private _removeEscListener(): void {
|
|
357
|
+
document.removeEventListener('keydown', this._boundEscHandler);
|
|
358
|
+
}
|
|
175
359
|
|
|
176
|
-
|
|
177
|
-
this.
|
|
360
|
+
private _handleEscapeKey(e: KeyboardEvent): void {
|
|
361
|
+
if (e.key === 'Escape' && this.open) {
|
|
362
|
+
this._setOpen(false);
|
|
363
|
+
this.tippyInstance?.hide();
|
|
364
|
+
}
|
|
178
365
|
}
|
|
179
366
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
367
|
+
// ─── ARIA ───
|
|
368
|
+
|
|
369
|
+
private _applyAria(): void {
|
|
370
|
+
if (!this.anchorEl || !this.panelContainer) return;
|
|
371
|
+
this.panelContainer.setAttribute('role', 'dialog');
|
|
372
|
+
this.panelContainer.id = this._panelId;
|
|
373
|
+
this.anchorEl.setAttribute('aria-haspopup', 'dialog');
|
|
374
|
+
this._syncAriaExpanded();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private _syncAriaExpanded(): void {
|
|
378
|
+
this.anchorEl?.setAttribute('aria-expanded', String(this.open));
|
|
379
|
+
if (this.open) {
|
|
380
|
+
this.anchorEl?.setAttribute('aria-describedby', this._panelId);
|
|
381
|
+
} else {
|
|
382
|
+
this.anchorEl?.removeAttribute('aria-describedby');
|
|
188
383
|
}
|
|
384
|
+
}
|
|
189
385
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
386
|
+
// ─── DOM construction ───
|
|
387
|
+
|
|
388
|
+
private _buildDOM(): void {
|
|
389
|
+
const children = Array.from(this.childNodes);
|
|
390
|
+
|
|
391
|
+
this.anchorEl = null;
|
|
392
|
+
const titleNodes: Node[] = [];
|
|
393
|
+
const actionNodes: Node[] = [];
|
|
394
|
+
const bodyNodes: Node[] = [];
|
|
395
|
+
let firstElementSeen = false;
|
|
396
|
+
|
|
397
|
+
for (const child of children) {
|
|
398
|
+
if (child instanceof HTMLElement) {
|
|
399
|
+
const slot = child.getAttribute('slot');
|
|
400
|
+
if (slot === 'title') {
|
|
401
|
+
child.removeAttribute('slot');
|
|
402
|
+
titleNodes.push(child);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
if (slot === 'action') {
|
|
406
|
+
child.removeAttribute('slot');
|
|
407
|
+
actionNodes.push(child);
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (!firstElementSeen && !this.for) {
|
|
411
|
+
this.anchorEl = child;
|
|
412
|
+
firstElementSeen = true;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
195
415
|
}
|
|
196
|
-
|
|
416
|
+
bodyNodes.push(child);
|
|
197
417
|
}
|
|
198
418
|
|
|
199
|
-
|
|
200
|
-
|
|
419
|
+
if (this.for) {
|
|
420
|
+
const anchor = document.getElementById(this.for);
|
|
421
|
+
if (anchor) {
|
|
422
|
+
this.anchorEl = anchor;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
201
425
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
return;
|
|
426
|
+
while (this.firstChild) {
|
|
427
|
+
this.removeChild(this.firstChild);
|
|
205
428
|
}
|
|
206
429
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
// Cleanup existing visibility manager if it exists
|
|
210
|
-
if (this.visibilityManager) {
|
|
211
|
-
this.visibilityManager.cleanup();
|
|
430
|
+
if (this.anchorEl && !this.for) {
|
|
431
|
+
this.appendChild(this.anchorEl);
|
|
212
432
|
}
|
|
213
433
|
|
|
214
|
-
this.
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
enableVisibilityEffect: this.enableVisibilityEffect,
|
|
218
|
-
enableTabClose: this.enableTabClose,
|
|
219
|
-
isOpen: () => this.open,
|
|
220
|
-
onAnchorOutOfView: () => {
|
|
221
|
-
this.open = false;
|
|
222
|
-
this.emit('nile-visibility-change', {
|
|
223
|
-
visible: false,
|
|
224
|
-
reason: 'anchor-out-of-view',
|
|
225
|
-
});
|
|
226
|
-
},
|
|
227
|
-
onDocumentHidden: () => {
|
|
228
|
-
this.open = false;
|
|
229
|
-
this.emit('nile-visibility-change', {
|
|
230
|
-
visible: false,
|
|
231
|
-
reason: 'document-hidden',
|
|
232
|
-
});
|
|
233
|
-
},
|
|
234
|
-
emit: (event, detail) => this.emit(`nile-${event}`, detail),
|
|
235
|
-
});
|
|
434
|
+
this.panelContainer = document.createElement('div');
|
|
435
|
+
this.panelContainer.className = 'nile-floating-panel__content';
|
|
436
|
+
this.panelContainer.style.display = 'none';
|
|
236
437
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
}
|
|
438
|
+
const body = document.createElement('div');
|
|
439
|
+
body.className = 'nile-floating-panel__body';
|
|
241
440
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
441
|
+
if (titleNodes.length > 0 || this.title) {
|
|
442
|
+
const titleDiv = document.createElement('div');
|
|
443
|
+
titleDiv.className = 'nile-floating-panel__title';
|
|
444
|
+
if (this.title) {
|
|
445
|
+
titleDiv.textContent = this.title;
|
|
446
|
+
} else {
|
|
447
|
+
titleNodes.forEach(n => titleDiv.appendChild(n));
|
|
448
|
+
}
|
|
449
|
+
body.appendChild(titleDiv);
|
|
245
450
|
}
|
|
246
451
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
452
|
+
if (bodyNodes.length > 0) {
|
|
453
|
+
const mainDiv = document.createElement('div');
|
|
454
|
+
mainDiv.className = 'nile-floating-panel__main';
|
|
455
|
+
bodyNodes.forEach(n => mainDiv.appendChild(n));
|
|
456
|
+
body.appendChild(mainDiv);
|
|
457
|
+
}
|
|
250
458
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
459
|
+
if (actionNodes.length > 0) {
|
|
460
|
+
const actionDiv = document.createElement('div');
|
|
461
|
+
actionDiv.className = 'nile-floating-panel__action';
|
|
462
|
+
actionNodes.forEach(n => actionDiv.appendChild(n));
|
|
463
|
+
body.appendChild(actionDiv);
|
|
254
464
|
}
|
|
465
|
+
|
|
466
|
+
this.panelContainer.appendChild(body);
|
|
467
|
+
this.appendChild(this.panelContainer);
|
|
468
|
+
|
|
469
|
+
this._applyAria();
|
|
255
470
|
}
|
|
256
471
|
|
|
257
|
-
|
|
258
|
-
this.eventManager.destroy();
|
|
472
|
+
// ─── Tippy management ───
|
|
259
473
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
474
|
+
private _resolveArrow() {
|
|
475
|
+
switch (this.arrow) {
|
|
476
|
+
case 'round': return roundArrow;
|
|
477
|
+
case 'none': return false as const;
|
|
478
|
+
default: return true as const;
|
|
263
479
|
}
|
|
480
|
+
}
|
|
264
481
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
482
|
+
private _setOpen(value: boolean): void {
|
|
483
|
+
this._suppressOpenWatch = true;
|
|
484
|
+
this.open = value;
|
|
485
|
+
this._syncAriaExpanded();
|
|
486
|
+
this._suppressOpenWatch = false;
|
|
487
|
+
}
|
|
268
488
|
|
|
269
|
-
|
|
489
|
+
private _getEffectiveDuration(): number | [number, number] {
|
|
490
|
+
if (NileFloatingPanel.prefersReducedMotion) return 0;
|
|
491
|
+
return parseDuration(this.duration);
|
|
492
|
+
}
|
|
270
493
|
|
|
271
|
-
|
|
494
|
+
private _getEffectiveAnimation(): string | false {
|
|
495
|
+
if (NileFloatingPanel.prefersReducedMotion) return false;
|
|
496
|
+
return this.animation;
|
|
272
497
|
}
|
|
273
498
|
|
|
274
|
-
|
|
275
|
-
|
|
499
|
+
private _attachTippy(): void {
|
|
500
|
+
this._destroyTippy();
|
|
501
|
+
|
|
502
|
+
if (this.disabled || !this.anchorEl || !this.panelContainer) return;
|
|
503
|
+
|
|
504
|
+
const resolvedFollowCursor = parseFollowCursor(this.followCursor);
|
|
505
|
+
const effectiveHideOnClick = this.preventOverlayClose ? false : this.hideOnClick;
|
|
506
|
+
|
|
507
|
+
const options: Partial<Props> = {
|
|
508
|
+
content: this.panelContainer,
|
|
509
|
+
placement: this.placement,
|
|
510
|
+
trigger: this.trigger,
|
|
511
|
+
offset: [this.skidding, this.distance],
|
|
512
|
+
theme: 'floating-panel',
|
|
513
|
+
animation: this._getEffectiveAnimation(),
|
|
514
|
+
interactive: this.interactive,
|
|
515
|
+
arrow: this._resolveArrow(),
|
|
516
|
+
duration: this._getEffectiveDuration(),
|
|
517
|
+
allowHTML: this.allowHTML,
|
|
518
|
+
delay: this.delay as any,
|
|
519
|
+
maxWidth: this.maxWidth,
|
|
520
|
+
zIndex: this.zIndex,
|
|
521
|
+
hideOnClick: effectiveHideOnClick,
|
|
522
|
+
inertia: NileFloatingPanel.prefersReducedMotion ? false : this.inertia,
|
|
523
|
+
interactiveBorder: this.interactiveBorder,
|
|
524
|
+
appendTo: document.body,
|
|
525
|
+
followCursor: resolvedFollowCursor,
|
|
526
|
+
plugins: resolvedFollowCursor ? [followCursorPlugin] : [],
|
|
527
|
+
popperOptions: {
|
|
528
|
+
modifiers: [{ name: 'flip', enabled: this.flip }],
|
|
529
|
+
},
|
|
530
|
+
onMount: () => {
|
|
531
|
+
if (this.panelContainer) this.panelContainer.style.display = '';
|
|
532
|
+
},
|
|
533
|
+
onShow: (instance) => {
|
|
534
|
+
if (this.panelContainer) this.panelContainer.style.display = '';
|
|
535
|
+
const tc = instance.popper.querySelector('.tippy-content') as HTMLElement | null;
|
|
536
|
+
if (tc) {
|
|
537
|
+
if (this.width) tc.style.width = this.width;
|
|
538
|
+
if (this.height) { tc.style.height = this.height; tc.style.overflow = 'auto'; }
|
|
539
|
+
}
|
|
540
|
+
this._hideGroupSiblings();
|
|
541
|
+
this._setOpen(true);
|
|
542
|
+
this._addEscListener();
|
|
543
|
+
this.dispatchEvent(new CustomEvent('nile-show', { detail: { instance, target: instance.reference } }));
|
|
544
|
+
this.dispatchEvent(new CustomEvent('nile-toggle', { detail: { open: true, instance, target: instance.reference } }));
|
|
545
|
+
return undefined;
|
|
546
|
+
},
|
|
547
|
+
onShown: (instance) => {
|
|
548
|
+
this.dispatchEvent(new CustomEvent('nile-after-show', { detail: { instance, target: instance.reference } }));
|
|
549
|
+
},
|
|
550
|
+
onHide: (instance) => {
|
|
551
|
+
this._setOpen(false);
|
|
552
|
+
this._removeEscListener();
|
|
553
|
+
this.dispatchEvent(new CustomEvent('nile-hide', { detail: { instance, target: instance.reference } }));
|
|
554
|
+
this.dispatchEvent(new CustomEvent('nile-toggle', { detail: { open: false, instance, target: instance.reference } }));
|
|
555
|
+
return undefined;
|
|
556
|
+
},
|
|
557
|
+
onHidden: (instance) => {
|
|
558
|
+
if (this.panelContainer) this.panelContainer.style.display = 'none';
|
|
559
|
+
this.dispatchEvent(new CustomEvent('nile-after-hide', { detail: { instance, target: instance.reference } }));
|
|
560
|
+
},
|
|
561
|
+
};
|
|
562
|
+
|
|
563
|
+
this.tippyInstance = tippy(this.anchorEl, options);
|
|
564
|
+
|
|
565
|
+
if (this.open) {
|
|
566
|
+
queueMicrotask(() => this.tippyInstance?.show());
|
|
567
|
+
}
|
|
276
568
|
}
|
|
277
569
|
|
|
278
|
-
private
|
|
279
|
-
if (this.
|
|
280
|
-
this.
|
|
570
|
+
private _destroyTippy(): void {
|
|
571
|
+
if (this.tippyInstance) {
|
|
572
|
+
this.tippyInstance.destroy();
|
|
573
|
+
this.tippyInstance = null;
|
|
281
574
|
}
|
|
282
|
-
|
|
575
|
+
if (this.panelContainer) {
|
|
576
|
+
this.panelContainer.style.display = 'none';
|
|
577
|
+
if (this.panelContainer.parentElement !== this) {
|
|
578
|
+
this.appendChild(this.panelContainer);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
283
582
|
}
|
|
284
583
|
|
|
285
584
|
export default NileFloatingPanel;
|