selective-ui 1.0.2 → 1.0.3
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/LICENSE +21 -21
- package/README.md +7 -2
- package/dist/selective-ui.css +567 -567
- package/dist/selective-ui.css.map +1 -1
- package/dist/selective-ui.esm.js +6046 -6046
- package/dist/selective-ui.esm.js.map +1 -1
- package/dist/selective-ui.esm.min.js +1 -1
- package/dist/selective-ui.esm.min.js.br +0 -0
- package/dist/selective-ui.min.js +1 -1
- package/dist/selective-ui.min.js.br +0 -0
- package/dist/selective-ui.umd.js +6046 -6046
- package/dist/selective-ui.umd.js.map +1 -1
- package/package.json +68 -68
- package/src/css/components/accessorybox.css +63 -63
- package/src/css/components/directive.css +19 -19
- package/src/css/components/empty-state.css +25 -25
- package/src/css/components/loading-state.css +25 -25
- package/src/css/components/optgroup.css +61 -61
- package/src/css/components/option-handle.css +33 -33
- package/src/css/components/option.css +129 -129
- package/src/css/components/placeholder.css +14 -14
- package/src/css/components/popup.css +38 -38
- package/src/css/components/searchbox.css +28 -28
- package/src/css/components/selectbox.css +53 -53
- package/src/css/index.css +74 -74
- package/src/js/adapter/mixed-adapter.js +434 -434
- package/src/js/components/accessorybox.js +124 -124
- package/src/js/components/directive.js +37 -37
- package/src/js/components/empty-state.js +67 -67
- package/src/js/components/loading-state.js +59 -59
- package/src/js/components/option-handle.js +113 -113
- package/src/js/components/placeholder.js +56 -56
- package/src/js/components/popup.js +470 -470
- package/src/js/components/searchbox.js +167 -167
- package/src/js/components/selectbox.js +692 -692
- package/src/js/core/base/adapter.js +162 -162
- package/src/js/core/base/model.js +59 -59
- package/src/js/core/base/recyclerview.js +82 -82
- package/src/js/core/base/view.js +62 -62
- package/src/js/core/model-manager.js +286 -286
- package/src/js/core/search-controller.js +521 -521
- package/src/js/index.js +136 -136
- package/src/js/models/group-model.js +142 -142
- package/src/js/models/option-model.js +236 -236
- package/src/js/services/dataset-observer.js +73 -73
- package/src/js/services/ea-observer.js +87 -87
- package/src/js/services/effector.js +403 -403
- package/src/js/services/refresher.js +39 -39
- package/src/js/services/resize-observer.js +151 -151
- package/src/js/services/select-observer.js +60 -60
- package/src/js/types/adapter.type.js +32 -32
- package/src/js/types/effector.type.js +23 -23
- package/src/js/types/ievents.type.js +10 -10
- package/src/js/types/libs.type.js +27 -27
- package/src/js/types/model.type.js +11 -11
- package/src/js/types/recyclerview.type.js +11 -11
- package/src/js/types/resize-observer.type.js +18 -18
- package/src/js/types/view.group.type.js +12 -12
- package/src/js/types/view.option.type.js +14 -14
- package/src/js/types/view.type.js +10 -10
- package/src/js/utils/guard.js +46 -46
- package/src/js/utils/ievents.js +83 -83
- package/src/js/utils/istorage.js +60 -60
- package/src/js/utils/libs.js +618 -618
- package/src/js/utils/selective.js +385 -385
- package/src/js/views/group-view.js +102 -102
- package/src/js/views/option-view.js +152 -152
|
@@ -1,471 +1,471 @@
|
|
|
1
|
-
import { Libs } from "../utils/libs.js";
|
|
2
|
-
import { OptionHandle } from "./option-handle.js";
|
|
3
|
-
import { ResizeObserverService } from "../services/resize-observer.js"
|
|
4
|
-
import { ModelManager } from "../core/model-manager.js";
|
|
5
|
-
import { OptionModel } from "../models/option-model.js";
|
|
6
|
-
import { EmptyState } from "./empty-state.js";
|
|
7
|
-
import { LoadingState } from "./loading-state.js";
|
|
8
|
-
import { MixedAdapter } from "../adapter/mixed-adapter.js";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* @class
|
|
12
|
-
*/
|
|
13
|
-
export class Popup {
|
|
14
|
-
/** @type {ModelManager<OptionModel, MixedAdapter>} */
|
|
15
|
-
#modelManager;
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Represents a popup component that manages rendering and interaction for a dropdown panel.
|
|
19
|
-
* Stores a reference to the ModelManager for handling option models and adapter logic.
|
|
20
|
-
*
|
|
21
|
-
* @param {HTMLSelectElement|null} [select=null] - The source select element to bind.
|
|
22
|
-
* @param {object|null} [options=null] - Configuration options for the popup.
|
|
23
|
-
* @param {ModelManager|null} [modelManager=null] - The model manager instance for data handling.
|
|
24
|
-
*/
|
|
25
|
-
constructor(select = null, options = null, modelManager = null) {
|
|
26
|
-
this.#modelManager = modelManager;
|
|
27
|
-
if (select && options) {
|
|
28
|
-
this.init(select, options);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
options = null;
|
|
33
|
-
isCreated = false;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* @type {MixedAdapter}
|
|
37
|
-
*/
|
|
38
|
-
optionAdapter = null;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* @type {HTMLDivElement}
|
|
42
|
-
*/
|
|
43
|
-
node = null;
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* @type {EffectorInterface}
|
|
47
|
-
*/
|
|
48
|
-
#effSvc = null;
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* @type {ResizeObserverService}
|
|
52
|
-
*/
|
|
53
|
-
#resizeObser;
|
|
54
|
-
|
|
55
|
-
#parent = null;
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* @type {OptionHandle}
|
|
59
|
-
*/
|
|
60
|
-
optionHandle = null;
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* @type {EmptyState}
|
|
64
|
-
*/
|
|
65
|
-
emptyState = null;
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* @type {LoadingState}
|
|
69
|
-
*/
|
|
70
|
-
loadingState = null;
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* @type {RecyclerViewContract<MixedAdapter>}
|
|
74
|
-
*/
|
|
75
|
-
recyclerView = null;
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* @type {HTMLDivElement}
|
|
79
|
-
*/
|
|
80
|
-
#optionsContainer = null;
|
|
81
|
-
|
|
82
|
-
#scrollListener = null;
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* @type {NodeJS.Timeout}
|
|
86
|
-
*/
|
|
87
|
-
#hideLoadHandle = null;
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Initializes the popup UI: creates DOM structure, wires OptionHandle, LoadingState, and EmptyState,
|
|
91
|
-
* binds ModelManager resources (adapter/recyclerView), and sets up empty-state logic.
|
|
92
|
-
*
|
|
93
|
-
* @param {HTMLSelectElement} select - The source select element to bind.
|
|
94
|
-
* @param {object} options - Configuration for panel, IDs, multiple mode, and texts.
|
|
95
|
-
*/
|
|
96
|
-
init(select, options) {
|
|
97
|
-
this.optionHandle = new OptionHandle(options);
|
|
98
|
-
this.emptyState = new EmptyState(options);
|
|
99
|
-
this.loadingState = new LoadingState(options);
|
|
100
|
-
|
|
101
|
-
const nodeMounted = Libs.mountNode({
|
|
102
|
-
PopupContainer: {
|
|
103
|
-
tag: {node: "div", classList: "selective-ui-popup", style: {maxHeight: options.panelHeight}},
|
|
104
|
-
child: {
|
|
105
|
-
OptionHandle: {
|
|
106
|
-
tag: this.optionHandle.node
|
|
107
|
-
},
|
|
108
|
-
OptionsContainer: {
|
|
109
|
-
tag: {id: options.SEID_LIST, node: "div", classList: "selective-ui-options-container", role: "listbox"}
|
|
110
|
-
},
|
|
111
|
-
LoadingState: {
|
|
112
|
-
tag: this.loadingState.node
|
|
113
|
-
},
|
|
114
|
-
EmptyState: {
|
|
115
|
-
tag: this.emptyState.node
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
this.node = nodeMounted.view;
|
|
122
|
-
this.#optionsContainer = nodeMounted.tags.OptionsContainer;
|
|
123
|
-
this.#parent = Libs.getBinderMap(select);
|
|
124
|
-
this.options = options;
|
|
125
|
-
|
|
126
|
-
this.#modelManager.load(this.#optionsContainer, {isMultiple: options.multiple});
|
|
127
|
-
const MMResources = this.#modelManager.getResources();
|
|
128
|
-
this.optionAdapter = MMResources.adapter;
|
|
129
|
-
this.recyclerView = MMResources.recyclerView;
|
|
130
|
-
|
|
131
|
-
this.optionHandle.OnSelectAll(() => {
|
|
132
|
-
MMResources.adapter.checkAll(true);
|
|
133
|
-
});
|
|
134
|
-
this.optionHandle.OnDeSelectAll(() => {
|
|
135
|
-
MMResources.adapter.checkAll(false);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
this.#setupEmptyStateLogic();
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Shows the loading state and temporarily skips model events.
|
|
143
|
-
* Adjusts size based on current visibility stats and triggers a resize.
|
|
144
|
-
*/
|
|
145
|
-
async showLoading() {
|
|
146
|
-
clearTimeout(this.#hideLoadHandle);
|
|
147
|
-
|
|
148
|
-
this.#modelManager.skipEvent(true);
|
|
149
|
-
if (Libs.string2Boolean(this.options.loadingfield) === false) return;
|
|
150
|
-
this.loadingState.show(this.optionAdapter.getVisibilityStats().hasVisible);
|
|
151
|
-
// this.#optionsContainer.classList.add("hide");
|
|
152
|
-
this.optionHandle.hide();
|
|
153
|
-
this.triggerResize();
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Hides the loading state after a short delay, restores event handling,
|
|
158
|
-
* updates empty state based on adapter visibility stats, and triggers a resize.
|
|
159
|
-
*/
|
|
160
|
-
async hideLoading() {
|
|
161
|
-
clearTimeout(this.#hideLoadHandle);
|
|
162
|
-
|
|
163
|
-
this.#hideLoadHandle = setTimeout(() => {
|
|
164
|
-
this.#modelManager.skipEvent(false);
|
|
165
|
-
this.loadingState.hide();
|
|
166
|
-
|
|
167
|
-
const stats = this.optionAdapter.getVisibilityStats();
|
|
168
|
-
this.#updateEmptyState(stats);
|
|
169
|
-
|
|
170
|
-
this.triggerResize();
|
|
171
|
-
}, 200);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Subscribes to adapter visibility and item changes to keep the empty state in sync.
|
|
176
|
-
* Triggers resize when items change to reflect layout updates.
|
|
177
|
-
*/
|
|
178
|
-
#setupEmptyStateLogic() {
|
|
179
|
-
this.optionAdapter.onVisibilityChanged((stats) => {
|
|
180
|
-
this.#updateEmptyState(stats);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
this.optionAdapter.onPropChanged("items", () => {
|
|
184
|
-
const stats = this.optionAdapter.getVisibilityStats();
|
|
185
|
-
this.#updateEmptyState(stats);
|
|
186
|
-
|
|
187
|
-
this.triggerResize();
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Updates the empty state and option container visibility based on aggregated stats.
|
|
193
|
-
* Shows "no data" when empty, "not found" when no visible items, otherwise shows options and handle.
|
|
194
|
-
*
|
|
195
|
-
* @param {{visibleCount:number,totalCount:number,hasVisible:boolean,isEmpty:boolean}|undefined} stats - Visibility stats; computed if omitted.
|
|
196
|
-
*/
|
|
197
|
-
#updateEmptyState(stats = null) {
|
|
198
|
-
if (!stats) {
|
|
199
|
-
stats = this.optionAdapter.getVisibilityStats();
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (stats?.isEmpty) {
|
|
203
|
-
this.emptyState.show("nodata");
|
|
204
|
-
this.#optionsContainer.classList.add("hide");
|
|
205
|
-
this.optionHandle.hide();
|
|
206
|
-
} else if (stats && !stats.hasVisible) {
|
|
207
|
-
this.emptyState.show("notfound");
|
|
208
|
-
this.#optionsContainer.classList.add("hide");
|
|
209
|
-
this.optionHandle.hide();
|
|
210
|
-
} else {
|
|
211
|
-
this.emptyState.hide();
|
|
212
|
-
this.#optionsContainer.classList.remove("hide");
|
|
213
|
-
this.optionHandle.refresh();
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Registers a callback for adapter property pre-change notifications.
|
|
219
|
-
*
|
|
220
|
-
* @param {string} propName - The adapter property to observe.
|
|
221
|
-
* @param {Function} callback - Invoked before the property changes.
|
|
222
|
-
*/
|
|
223
|
-
onAdapterPropChanging(propName, callback) {
|
|
224
|
-
this.optionAdapter.onPropChanging(propName, callback);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Registers a callback for adapter property post-change notifications.
|
|
229
|
-
*
|
|
230
|
-
* @param {string} propName - The adapter property to observe.
|
|
231
|
-
* @param {Function} callback - Invoked after the property changes.
|
|
232
|
-
*/
|
|
233
|
-
onAdapterPropChanged(propName, callback) {
|
|
234
|
-
this.optionAdapter.onPropChanged(propName, callback);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Injects an effector service used to perform side effects (e.g., animations or external actions).
|
|
239
|
-
*
|
|
240
|
-
* @param {EffectorInterface} effectorSvc - The effector service instance.
|
|
241
|
-
*/
|
|
242
|
-
setupEffector(effectorSvc) {
|
|
243
|
-
this.#effSvc = effectorSvc;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Opens the popup: creates and attaches DOM if needed, initializes observers and effector,
|
|
248
|
-
* computes position and dimensions, and runs expand animation. Invokes callback on completion.
|
|
249
|
-
*
|
|
250
|
-
* @param {Function|null} [callback=null] - Function to call after the popup finishes expanding.
|
|
251
|
-
*/
|
|
252
|
-
open(callback = null) {
|
|
253
|
-
if (!this.isCreated) {
|
|
254
|
-
document.body.appendChild(this.node);
|
|
255
|
-
this.isCreated = true;
|
|
256
|
-
|
|
257
|
-
this.#resizeObser = new ResizeObserverService();
|
|
258
|
-
this.#effSvc.setElement(this.node);
|
|
259
|
-
|
|
260
|
-
this.node.addEventListener("mousedown", (e) => {
|
|
261
|
-
e.stopPropagation();
|
|
262
|
-
e.preventDefault();
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
this.optionHandle.refresh();
|
|
267
|
-
this.#updateEmptyState();
|
|
268
|
-
|
|
269
|
-
const location = this.#getParentLocation();
|
|
270
|
-
const {position, top, maxHeight, realHeight} = this.#calculatePosition(location);
|
|
271
|
-
|
|
272
|
-
this.#effSvc.expand({
|
|
273
|
-
duration: this.options.animationtime,
|
|
274
|
-
display: "flex",
|
|
275
|
-
width: location.width,
|
|
276
|
-
left: location.left,
|
|
277
|
-
top: top,
|
|
278
|
-
maxHeight: maxHeight,
|
|
279
|
-
realHeight: realHeight,
|
|
280
|
-
position: position,
|
|
281
|
-
onComplete: () => {
|
|
282
|
-
this.#resizeObser.onChanged = (newLocation) => this.#handleResize(newLocation);
|
|
283
|
-
this.#resizeObser.connect(this.#parent.container.tags.ViewPanel);
|
|
284
|
-
callback && callback();
|
|
285
|
-
}
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Closes the popup: disconnects the resize observer and runs collapse animation.
|
|
291
|
-
* Safely no-ops if the popup has not been created.
|
|
292
|
-
*
|
|
293
|
-
* @param {Function|null} [callback=null] - Function to call after the popup finishes collapsing.
|
|
294
|
-
*/
|
|
295
|
-
close(callback = null) {
|
|
296
|
-
if (!this.isCreated) {
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
this.#resizeObser.disconnect();
|
|
301
|
-
|
|
302
|
-
this.#effSvc.collapse({
|
|
303
|
-
duration: this.options.animationtime,
|
|
304
|
-
onComplete: callback
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Programmatically triggers a resize recalculation if the popup is created,
|
|
310
|
-
* causing the layout to update based on the current parent dimensions.
|
|
311
|
-
*/
|
|
312
|
-
triggerResize() {
|
|
313
|
-
if (this.isCreated) {
|
|
314
|
-
this.#resizeObser.trigger();
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
/**
|
|
319
|
-
* Enables infinite scroll by listening to container scroll events and loading more data
|
|
320
|
-
* when nearing the bottom, respecting pagination state (enabled/loading/hasMore).
|
|
321
|
-
*
|
|
322
|
-
* @param {object} searchController - Controller providing pagination state and loadMore().
|
|
323
|
-
* @param {object} options - Additional configuration for the infinite scroll behavior.
|
|
324
|
-
*/
|
|
325
|
-
setupInfiniteScroll(searchController, options) {
|
|
326
|
-
this.#scrollListener = async () => {
|
|
327
|
-
const state = searchController.getPaginationState();
|
|
328
|
-
|
|
329
|
-
if (!state.isPaginationEnabled) {
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const container = this.node;
|
|
334
|
-
const scrollTop = container.scrollTop;
|
|
335
|
-
const scrollHeight = container.scrollHeight;
|
|
336
|
-
const clientHeight = container.clientHeight;
|
|
337
|
-
|
|
338
|
-
if (scrollHeight - scrollTop - clientHeight < 100) {
|
|
339
|
-
if (!state.isLoading && state.hasMore) {
|
|
340
|
-
const result = await searchController.loadMore();
|
|
341
|
-
|
|
342
|
-
if (!result.success && result.message) {
|
|
343
|
-
console.log("Load more:", result.message);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
this.node.addEventListener("scroll", this.#scrollListener);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Computes the parent panel's location and box metrics, including size, position,
|
|
354
|
-
* padding, and border, accounting for iOS visual viewport offsets.
|
|
355
|
-
*
|
|
356
|
-
* @returns {{
|
|
357
|
-
* width:number, height:number, top:number, left:number,
|
|
358
|
-
* padding:{top:number,right:number,bottom:number,left:number},
|
|
359
|
-
* border:{top:number,right:number,bottom:number,left:number}
|
|
360
|
-
* }} - The parent panel metrics in viewport coordinates.
|
|
361
|
-
*/
|
|
362
|
-
#getParentLocation() {
|
|
363
|
-
const viewPanel = this.#parent.container.tags.ViewPanel;
|
|
364
|
-
const rect = viewPanel.getBoundingClientRect();
|
|
365
|
-
const style = window.getComputedStyle(viewPanel);
|
|
366
|
-
|
|
367
|
-
const vv = window.visualViewport;
|
|
368
|
-
const is_ios = Libs.IsIOS();
|
|
369
|
-
|
|
370
|
-
const viewportOffsetY = vv && is_ios ? vv.offsetTop : 0;
|
|
371
|
-
const viewportOffsetX = vv && is_ios ? vv.offsetLeft : 0;
|
|
372
|
-
|
|
373
|
-
return {
|
|
374
|
-
width: rect.width,
|
|
375
|
-
height: rect.height,
|
|
376
|
-
top: rect.top - viewportOffsetY,
|
|
377
|
-
left: rect.left - viewportOffsetX,
|
|
378
|
-
padding: {
|
|
379
|
-
top: parseFloat(style.paddingTop),
|
|
380
|
-
right: parseFloat(style.paddingRight),
|
|
381
|
-
bottom: parseFloat(style.paddingBottom),
|
|
382
|
-
left: parseFloat(style.paddingLeft),
|
|
383
|
-
},
|
|
384
|
-
border: {
|
|
385
|
-
top: parseFloat(style.borderTopWidth),
|
|
386
|
-
right: parseFloat(style.borderRightWidth),
|
|
387
|
-
bottom: parseFloat(style.borderBottomWidth),
|
|
388
|
-
left: parseFloat(style.borderLeftWidth),
|
|
389
|
-
}
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
* Determines popup placement (top/bottom) and height constraints based on available viewport space,
|
|
395
|
-
* content size, and configured min/max heights; returns final position, top, and heights.
|
|
396
|
-
*
|
|
397
|
-
* @param {{width:number,height:number,top:number,left:number}} location - Parent panel metrics.
|
|
398
|
-
* @returns {{position:string, top:number, maxHeight:number, realHeight:number, contentHeight:number}}
|
|
399
|
-
* - Calculated layout values for the popup.
|
|
400
|
-
*/
|
|
401
|
-
#calculatePosition(location) {
|
|
402
|
-
const vv = window.visualViewport;
|
|
403
|
-
const is_ios = Libs.IsIOS();
|
|
404
|
-
const viewportHeight = vv?.height ?? window.innerHeight;
|
|
405
|
-
const viewportOffsetY = vv && is_ios ? vv.offsetTop : 0;
|
|
406
|
-
|
|
407
|
-
const gap = 3;
|
|
408
|
-
const safeMargin = 15;
|
|
409
|
-
|
|
410
|
-
const dimensions = this.#effSvc.getHiddenDimensions("flex");
|
|
411
|
-
const contentHeight = dimensions.scrollHeight;
|
|
412
|
-
|
|
413
|
-
const configMaxHeight = parseFloat(this.options.panelHeight) || 220;
|
|
414
|
-
const configMinHeight = parseFloat(this.options.panelMinHeight) || 100;
|
|
415
|
-
|
|
416
|
-
const spaceBelow = viewportHeight - (location.top + location.height) - gap;
|
|
417
|
-
const spaceAbove = location.top - gap;
|
|
418
|
-
|
|
419
|
-
let position = "bottom";
|
|
420
|
-
let maxHeight = configMaxHeight;
|
|
421
|
-
let realHeight = Math.min(contentHeight, maxHeight);
|
|
422
|
-
const heightOri = spaceBelow - safeMargin;
|
|
423
|
-
|
|
424
|
-
if (realHeight >= configMinHeight ? heightOri >= configMinHeight : heightOri >= realHeight) {
|
|
425
|
-
position = "bottom";
|
|
426
|
-
maxHeight = Math.min(spaceBelow - safeMargin, configMaxHeight);
|
|
427
|
-
}
|
|
428
|
-
else if (spaceAbove >= Math.max(realHeight, configMinHeight)) {
|
|
429
|
-
position = "top";
|
|
430
|
-
maxHeight = Math.min(spaceAbove - safeMargin, configMaxHeight);
|
|
431
|
-
}
|
|
432
|
-
else {
|
|
433
|
-
if (spaceBelow >= spaceAbove) {
|
|
434
|
-
position = "bottom";
|
|
435
|
-
maxHeight = Math.max(spaceBelow - safeMargin, configMinHeight);
|
|
436
|
-
} else {
|
|
437
|
-
position = "top";
|
|
438
|
-
maxHeight = Math.max(spaceAbove - safeMargin, configMinHeight);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
realHeight = Math.min(contentHeight, maxHeight);
|
|
443
|
-
|
|
444
|
-
const top = position === "bottom"
|
|
445
|
-
? (location.top + location.height + gap + viewportOffsetY)
|
|
446
|
-
: (location.top - realHeight - gap + viewportOffsetY);
|
|
447
|
-
|
|
448
|
-
return {position, top, maxHeight, realHeight, contentHeight};
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* Handles parent resize events by recalculating placement and dimensions,
|
|
453
|
-
* then animates the popup to the new size and position.
|
|
454
|
-
*
|
|
455
|
-
* @param {{width:number,height:number,top:number,left:number}} location - Updated parent metrics.
|
|
456
|
-
*/
|
|
457
|
-
#handleResize(location) {
|
|
458
|
-
const {position, top, maxHeight, realHeight} = this.#calculatePosition(location);
|
|
459
|
-
|
|
460
|
-
this.#effSvc.resize({
|
|
461
|
-
duration: this.options.animationtime,
|
|
462
|
-
width: location.width,
|
|
463
|
-
left: location.left,
|
|
464
|
-
top: top,
|
|
465
|
-
maxHeight: maxHeight,
|
|
466
|
-
realHeight: realHeight,
|
|
467
|
-
position: position,
|
|
468
|
-
animate: true
|
|
469
|
-
});
|
|
470
|
-
}
|
|
1
|
+
import { Libs } from "../utils/libs.js";
|
|
2
|
+
import { OptionHandle } from "./option-handle.js";
|
|
3
|
+
import { ResizeObserverService } from "../services/resize-observer.js"
|
|
4
|
+
import { ModelManager } from "../core/model-manager.js";
|
|
5
|
+
import { OptionModel } from "../models/option-model.js";
|
|
6
|
+
import { EmptyState } from "./empty-state.js";
|
|
7
|
+
import { LoadingState } from "./loading-state.js";
|
|
8
|
+
import { MixedAdapter } from "../adapter/mixed-adapter.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @class
|
|
12
|
+
*/
|
|
13
|
+
export class Popup {
|
|
14
|
+
/** @type {ModelManager<OptionModel, MixedAdapter>} */
|
|
15
|
+
#modelManager;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Represents a popup component that manages rendering and interaction for a dropdown panel.
|
|
19
|
+
* Stores a reference to the ModelManager for handling option models and adapter logic.
|
|
20
|
+
*
|
|
21
|
+
* @param {HTMLSelectElement|null} [select=null] - The source select element to bind.
|
|
22
|
+
* @param {object|null} [options=null] - Configuration options for the popup.
|
|
23
|
+
* @param {ModelManager|null} [modelManager=null] - The model manager instance for data handling.
|
|
24
|
+
*/
|
|
25
|
+
constructor(select = null, options = null, modelManager = null) {
|
|
26
|
+
this.#modelManager = modelManager;
|
|
27
|
+
if (select && options) {
|
|
28
|
+
this.init(select, options);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
options = null;
|
|
33
|
+
isCreated = false;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @type {MixedAdapter}
|
|
37
|
+
*/
|
|
38
|
+
optionAdapter = null;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @type {HTMLDivElement}
|
|
42
|
+
*/
|
|
43
|
+
node = null;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @type {EffectorInterface}
|
|
47
|
+
*/
|
|
48
|
+
#effSvc = null;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @type {ResizeObserverService}
|
|
52
|
+
*/
|
|
53
|
+
#resizeObser;
|
|
54
|
+
|
|
55
|
+
#parent = null;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @type {OptionHandle}
|
|
59
|
+
*/
|
|
60
|
+
optionHandle = null;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @type {EmptyState}
|
|
64
|
+
*/
|
|
65
|
+
emptyState = null;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @type {LoadingState}
|
|
69
|
+
*/
|
|
70
|
+
loadingState = null;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @type {RecyclerViewContract<MixedAdapter>}
|
|
74
|
+
*/
|
|
75
|
+
recyclerView = null;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @type {HTMLDivElement}
|
|
79
|
+
*/
|
|
80
|
+
#optionsContainer = null;
|
|
81
|
+
|
|
82
|
+
#scrollListener = null;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* @type {NodeJS.Timeout}
|
|
86
|
+
*/
|
|
87
|
+
#hideLoadHandle = null;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Initializes the popup UI: creates DOM structure, wires OptionHandle, LoadingState, and EmptyState,
|
|
91
|
+
* binds ModelManager resources (adapter/recyclerView), and sets up empty-state logic.
|
|
92
|
+
*
|
|
93
|
+
* @param {HTMLSelectElement} select - The source select element to bind.
|
|
94
|
+
* @param {object} options - Configuration for panel, IDs, multiple mode, and texts.
|
|
95
|
+
*/
|
|
96
|
+
init(select, options) {
|
|
97
|
+
this.optionHandle = new OptionHandle(options);
|
|
98
|
+
this.emptyState = new EmptyState(options);
|
|
99
|
+
this.loadingState = new LoadingState(options);
|
|
100
|
+
|
|
101
|
+
const nodeMounted = Libs.mountNode({
|
|
102
|
+
PopupContainer: {
|
|
103
|
+
tag: {node: "div", classList: "selective-ui-popup", style: {maxHeight: options.panelHeight}},
|
|
104
|
+
child: {
|
|
105
|
+
OptionHandle: {
|
|
106
|
+
tag: this.optionHandle.node
|
|
107
|
+
},
|
|
108
|
+
OptionsContainer: {
|
|
109
|
+
tag: {id: options.SEID_LIST, node: "div", classList: "selective-ui-options-container", role: "listbox"}
|
|
110
|
+
},
|
|
111
|
+
LoadingState: {
|
|
112
|
+
tag: this.loadingState.node
|
|
113
|
+
},
|
|
114
|
+
EmptyState: {
|
|
115
|
+
tag: this.emptyState.node
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
this.node = nodeMounted.view;
|
|
122
|
+
this.#optionsContainer = nodeMounted.tags.OptionsContainer;
|
|
123
|
+
this.#parent = Libs.getBinderMap(select);
|
|
124
|
+
this.options = options;
|
|
125
|
+
|
|
126
|
+
this.#modelManager.load(this.#optionsContainer, {isMultiple: options.multiple});
|
|
127
|
+
const MMResources = this.#modelManager.getResources();
|
|
128
|
+
this.optionAdapter = MMResources.adapter;
|
|
129
|
+
this.recyclerView = MMResources.recyclerView;
|
|
130
|
+
|
|
131
|
+
this.optionHandle.OnSelectAll(() => {
|
|
132
|
+
MMResources.adapter.checkAll(true);
|
|
133
|
+
});
|
|
134
|
+
this.optionHandle.OnDeSelectAll(() => {
|
|
135
|
+
MMResources.adapter.checkAll(false);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
this.#setupEmptyStateLogic();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Shows the loading state and temporarily skips model events.
|
|
143
|
+
* Adjusts size based on current visibility stats and triggers a resize.
|
|
144
|
+
*/
|
|
145
|
+
async showLoading() {
|
|
146
|
+
clearTimeout(this.#hideLoadHandle);
|
|
147
|
+
|
|
148
|
+
this.#modelManager.skipEvent(true);
|
|
149
|
+
if (Libs.string2Boolean(this.options.loadingfield) === false) return;
|
|
150
|
+
this.loadingState.show(this.optionAdapter.getVisibilityStats().hasVisible);
|
|
151
|
+
// this.#optionsContainer.classList.add("hide");
|
|
152
|
+
this.optionHandle.hide();
|
|
153
|
+
this.triggerResize();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Hides the loading state after a short delay, restores event handling,
|
|
158
|
+
* updates empty state based on adapter visibility stats, and triggers a resize.
|
|
159
|
+
*/
|
|
160
|
+
async hideLoading() {
|
|
161
|
+
clearTimeout(this.#hideLoadHandle);
|
|
162
|
+
|
|
163
|
+
this.#hideLoadHandle = setTimeout(() => {
|
|
164
|
+
this.#modelManager.skipEvent(false);
|
|
165
|
+
this.loadingState.hide();
|
|
166
|
+
|
|
167
|
+
const stats = this.optionAdapter.getVisibilityStats();
|
|
168
|
+
this.#updateEmptyState(stats);
|
|
169
|
+
|
|
170
|
+
this.triggerResize();
|
|
171
|
+
}, 200);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Subscribes to adapter visibility and item changes to keep the empty state in sync.
|
|
176
|
+
* Triggers resize when items change to reflect layout updates.
|
|
177
|
+
*/
|
|
178
|
+
#setupEmptyStateLogic() {
|
|
179
|
+
this.optionAdapter.onVisibilityChanged((stats) => {
|
|
180
|
+
this.#updateEmptyState(stats);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
this.optionAdapter.onPropChanged("items", () => {
|
|
184
|
+
const stats = this.optionAdapter.getVisibilityStats();
|
|
185
|
+
this.#updateEmptyState(stats);
|
|
186
|
+
|
|
187
|
+
this.triggerResize();
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Updates the empty state and option container visibility based on aggregated stats.
|
|
193
|
+
* Shows "no data" when empty, "not found" when no visible items, otherwise shows options and handle.
|
|
194
|
+
*
|
|
195
|
+
* @param {{visibleCount:number,totalCount:number,hasVisible:boolean,isEmpty:boolean}|undefined} stats - Visibility stats; computed if omitted.
|
|
196
|
+
*/
|
|
197
|
+
#updateEmptyState(stats = null) {
|
|
198
|
+
if (!stats) {
|
|
199
|
+
stats = this.optionAdapter.getVisibilityStats();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (stats?.isEmpty) {
|
|
203
|
+
this.emptyState.show("nodata");
|
|
204
|
+
this.#optionsContainer.classList.add("hide");
|
|
205
|
+
this.optionHandle.hide();
|
|
206
|
+
} else if (stats && !stats.hasVisible) {
|
|
207
|
+
this.emptyState.show("notfound");
|
|
208
|
+
this.#optionsContainer.classList.add("hide");
|
|
209
|
+
this.optionHandle.hide();
|
|
210
|
+
} else {
|
|
211
|
+
this.emptyState.hide();
|
|
212
|
+
this.#optionsContainer.classList.remove("hide");
|
|
213
|
+
this.optionHandle.refresh();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Registers a callback for adapter property pre-change notifications.
|
|
219
|
+
*
|
|
220
|
+
* @param {string} propName - The adapter property to observe.
|
|
221
|
+
* @param {Function} callback - Invoked before the property changes.
|
|
222
|
+
*/
|
|
223
|
+
onAdapterPropChanging(propName, callback) {
|
|
224
|
+
this.optionAdapter.onPropChanging(propName, callback);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Registers a callback for adapter property post-change notifications.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} propName - The adapter property to observe.
|
|
231
|
+
* @param {Function} callback - Invoked after the property changes.
|
|
232
|
+
*/
|
|
233
|
+
onAdapterPropChanged(propName, callback) {
|
|
234
|
+
this.optionAdapter.onPropChanged(propName, callback);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Injects an effector service used to perform side effects (e.g., animations or external actions).
|
|
239
|
+
*
|
|
240
|
+
* @param {EffectorInterface} effectorSvc - The effector service instance.
|
|
241
|
+
*/
|
|
242
|
+
setupEffector(effectorSvc) {
|
|
243
|
+
this.#effSvc = effectorSvc;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Opens the popup: creates and attaches DOM if needed, initializes observers and effector,
|
|
248
|
+
* computes position and dimensions, and runs expand animation. Invokes callback on completion.
|
|
249
|
+
*
|
|
250
|
+
* @param {Function|null} [callback=null] - Function to call after the popup finishes expanding.
|
|
251
|
+
*/
|
|
252
|
+
open(callback = null) {
|
|
253
|
+
if (!this.isCreated) {
|
|
254
|
+
document.body.appendChild(this.node);
|
|
255
|
+
this.isCreated = true;
|
|
256
|
+
|
|
257
|
+
this.#resizeObser = new ResizeObserverService();
|
|
258
|
+
this.#effSvc.setElement(this.node);
|
|
259
|
+
|
|
260
|
+
this.node.addEventListener("mousedown", (e) => {
|
|
261
|
+
e.stopPropagation();
|
|
262
|
+
e.preventDefault();
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
this.optionHandle.refresh();
|
|
267
|
+
this.#updateEmptyState();
|
|
268
|
+
|
|
269
|
+
const location = this.#getParentLocation();
|
|
270
|
+
const {position, top, maxHeight, realHeight} = this.#calculatePosition(location);
|
|
271
|
+
|
|
272
|
+
this.#effSvc.expand({
|
|
273
|
+
duration: this.options.animationtime,
|
|
274
|
+
display: "flex",
|
|
275
|
+
width: location.width,
|
|
276
|
+
left: location.left,
|
|
277
|
+
top: top,
|
|
278
|
+
maxHeight: maxHeight,
|
|
279
|
+
realHeight: realHeight,
|
|
280
|
+
position: position,
|
|
281
|
+
onComplete: () => {
|
|
282
|
+
this.#resizeObser.onChanged = (newLocation) => this.#handleResize(newLocation);
|
|
283
|
+
this.#resizeObser.connect(this.#parent.container.tags.ViewPanel);
|
|
284
|
+
callback && callback();
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Closes the popup: disconnects the resize observer and runs collapse animation.
|
|
291
|
+
* Safely no-ops if the popup has not been created.
|
|
292
|
+
*
|
|
293
|
+
* @param {Function|null} [callback=null] - Function to call after the popup finishes collapsing.
|
|
294
|
+
*/
|
|
295
|
+
close(callback = null) {
|
|
296
|
+
if (!this.isCreated) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
this.#resizeObser.disconnect();
|
|
301
|
+
|
|
302
|
+
this.#effSvc.collapse({
|
|
303
|
+
duration: this.options.animationtime,
|
|
304
|
+
onComplete: callback
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Programmatically triggers a resize recalculation if the popup is created,
|
|
310
|
+
* causing the layout to update based on the current parent dimensions.
|
|
311
|
+
*/
|
|
312
|
+
triggerResize() {
|
|
313
|
+
if (this.isCreated) {
|
|
314
|
+
this.#resizeObser.trigger();
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Enables infinite scroll by listening to container scroll events and loading more data
|
|
320
|
+
* when nearing the bottom, respecting pagination state (enabled/loading/hasMore).
|
|
321
|
+
*
|
|
322
|
+
* @param {object} searchController - Controller providing pagination state and loadMore().
|
|
323
|
+
* @param {object} options - Additional configuration for the infinite scroll behavior.
|
|
324
|
+
*/
|
|
325
|
+
setupInfiniteScroll(searchController, options) {
|
|
326
|
+
this.#scrollListener = async () => {
|
|
327
|
+
const state = searchController.getPaginationState();
|
|
328
|
+
|
|
329
|
+
if (!state.isPaginationEnabled) {
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const container = this.node;
|
|
334
|
+
const scrollTop = container.scrollTop;
|
|
335
|
+
const scrollHeight = container.scrollHeight;
|
|
336
|
+
const clientHeight = container.clientHeight;
|
|
337
|
+
|
|
338
|
+
if (scrollHeight - scrollTop - clientHeight < 100) {
|
|
339
|
+
if (!state.isLoading && state.hasMore) {
|
|
340
|
+
const result = await searchController.loadMore();
|
|
341
|
+
|
|
342
|
+
if (!result.success && result.message) {
|
|
343
|
+
console.log("Load more:", result.message);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
this.node.addEventListener("scroll", this.#scrollListener);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Computes the parent panel's location and box metrics, including size, position,
|
|
354
|
+
* padding, and border, accounting for iOS visual viewport offsets.
|
|
355
|
+
*
|
|
356
|
+
* @returns {{
|
|
357
|
+
* width:number, height:number, top:number, left:number,
|
|
358
|
+
* padding:{top:number,right:number,bottom:number,left:number},
|
|
359
|
+
* border:{top:number,right:number,bottom:number,left:number}
|
|
360
|
+
* }} - The parent panel metrics in viewport coordinates.
|
|
361
|
+
*/
|
|
362
|
+
#getParentLocation() {
|
|
363
|
+
const viewPanel = this.#parent.container.tags.ViewPanel;
|
|
364
|
+
const rect = viewPanel.getBoundingClientRect();
|
|
365
|
+
const style = window.getComputedStyle(viewPanel);
|
|
366
|
+
|
|
367
|
+
const vv = window.visualViewport;
|
|
368
|
+
const is_ios = Libs.IsIOS();
|
|
369
|
+
|
|
370
|
+
const viewportOffsetY = vv && is_ios ? vv.offsetTop : 0;
|
|
371
|
+
const viewportOffsetX = vv && is_ios ? vv.offsetLeft : 0;
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
width: rect.width,
|
|
375
|
+
height: rect.height,
|
|
376
|
+
top: rect.top - viewportOffsetY,
|
|
377
|
+
left: rect.left - viewportOffsetX,
|
|
378
|
+
padding: {
|
|
379
|
+
top: parseFloat(style.paddingTop),
|
|
380
|
+
right: parseFloat(style.paddingRight),
|
|
381
|
+
bottom: parseFloat(style.paddingBottom),
|
|
382
|
+
left: parseFloat(style.paddingLeft),
|
|
383
|
+
},
|
|
384
|
+
border: {
|
|
385
|
+
top: parseFloat(style.borderTopWidth),
|
|
386
|
+
right: parseFloat(style.borderRightWidth),
|
|
387
|
+
bottom: parseFloat(style.borderBottomWidth),
|
|
388
|
+
left: parseFloat(style.borderLeftWidth),
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Determines popup placement (top/bottom) and height constraints based on available viewport space,
|
|
395
|
+
* content size, and configured min/max heights; returns final position, top, and heights.
|
|
396
|
+
*
|
|
397
|
+
* @param {{width:number,height:number,top:number,left:number}} location - Parent panel metrics.
|
|
398
|
+
* @returns {{position:string, top:number, maxHeight:number, realHeight:number, contentHeight:number}}
|
|
399
|
+
* - Calculated layout values for the popup.
|
|
400
|
+
*/
|
|
401
|
+
#calculatePosition(location) {
|
|
402
|
+
const vv = window.visualViewport;
|
|
403
|
+
const is_ios = Libs.IsIOS();
|
|
404
|
+
const viewportHeight = vv?.height ?? window.innerHeight;
|
|
405
|
+
const viewportOffsetY = vv && is_ios ? vv.offsetTop : 0;
|
|
406
|
+
|
|
407
|
+
const gap = 3;
|
|
408
|
+
const safeMargin = 15;
|
|
409
|
+
|
|
410
|
+
const dimensions = this.#effSvc.getHiddenDimensions("flex");
|
|
411
|
+
const contentHeight = dimensions.scrollHeight;
|
|
412
|
+
|
|
413
|
+
const configMaxHeight = parseFloat(this.options.panelHeight) || 220;
|
|
414
|
+
const configMinHeight = parseFloat(this.options.panelMinHeight) || 100;
|
|
415
|
+
|
|
416
|
+
const spaceBelow = viewportHeight - (location.top + location.height) - gap;
|
|
417
|
+
const spaceAbove = location.top - gap;
|
|
418
|
+
|
|
419
|
+
let position = "bottom";
|
|
420
|
+
let maxHeight = configMaxHeight;
|
|
421
|
+
let realHeight = Math.min(contentHeight, maxHeight);
|
|
422
|
+
const heightOri = spaceBelow - safeMargin;
|
|
423
|
+
|
|
424
|
+
if (realHeight >= configMinHeight ? heightOri >= configMinHeight : heightOri >= realHeight) {
|
|
425
|
+
position = "bottom";
|
|
426
|
+
maxHeight = Math.min(spaceBelow - safeMargin, configMaxHeight);
|
|
427
|
+
}
|
|
428
|
+
else if (spaceAbove >= Math.max(realHeight, configMinHeight)) {
|
|
429
|
+
position = "top";
|
|
430
|
+
maxHeight = Math.min(spaceAbove - safeMargin, configMaxHeight);
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
if (spaceBelow >= spaceAbove) {
|
|
434
|
+
position = "bottom";
|
|
435
|
+
maxHeight = Math.max(spaceBelow - safeMargin, configMinHeight);
|
|
436
|
+
} else {
|
|
437
|
+
position = "top";
|
|
438
|
+
maxHeight = Math.max(spaceAbove - safeMargin, configMinHeight);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
realHeight = Math.min(contentHeight, maxHeight);
|
|
443
|
+
|
|
444
|
+
const top = position === "bottom"
|
|
445
|
+
? (location.top + location.height + gap + viewportOffsetY)
|
|
446
|
+
: (location.top - realHeight - gap + viewportOffsetY);
|
|
447
|
+
|
|
448
|
+
return {position, top, maxHeight, realHeight, contentHeight};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Handles parent resize events by recalculating placement and dimensions,
|
|
453
|
+
* then animates the popup to the new size and position.
|
|
454
|
+
*
|
|
455
|
+
* @param {{width:number,height:number,top:number,left:number}} location - Updated parent metrics.
|
|
456
|
+
*/
|
|
457
|
+
#handleResize(location) {
|
|
458
|
+
const {position, top, maxHeight, realHeight} = this.#calculatePosition(location);
|
|
459
|
+
|
|
460
|
+
this.#effSvc.resize({
|
|
461
|
+
duration: this.options.animationtime,
|
|
462
|
+
width: location.width,
|
|
463
|
+
left: location.left,
|
|
464
|
+
top: top,
|
|
465
|
+
maxHeight: maxHeight,
|
|
466
|
+
realHeight: realHeight,
|
|
467
|
+
position: position,
|
|
468
|
+
animate: true
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
471
|
}
|