selective-ui 1.2.1 → 1.2.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/dist/selective-ui.css +3 -1
- package/dist/selective-ui.css.map +1 -1
- package/dist/selective-ui.esm.js +498 -467
- package/dist/selective-ui.esm.js.map +1 -1
- package/dist/selective-ui.esm.min.js +2 -2
- package/dist/selective-ui.esm.min.js.br +0 -0
- package/dist/selective-ui.min.css +1 -1
- package/dist/selective-ui.min.css.br +0 -0
- package/dist/selective-ui.min.js +2 -2
- package/dist/selective-ui.min.js.br +0 -0
- package/dist/selective-ui.umd.js +499 -468
- package/dist/selective-ui.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/css/components/popup.css +3 -1
- package/src/ts/adapter/mixed-adapter.ts +50 -54
- package/src/ts/components/accessorybox.ts +51 -25
- package/src/ts/components/directive.ts +3 -3
- package/src/ts/components/empty-state.ts +7 -7
- package/src/ts/components/loading-state.ts +7 -7
- package/src/ts/components/option-handle.ts +17 -17
- package/src/ts/components/placeholder.ts +12 -12
- package/src/ts/components/popup.ts +94 -109
- package/src/ts/components/searchbox.ts +14 -14
- package/src/ts/components/selectbox.ts +25 -29
- package/src/ts/core/base/adapter.ts +19 -19
- package/src/ts/core/base/model.ts +12 -13
- package/src/ts/core/base/recyclerview.ts +7 -7
- package/src/ts/core/base/view.ts +6 -6
- package/src/ts/core/base/virtual-recyclerview.ts +55 -52
- package/src/ts/core/model-manager.ts +61 -54
- package/src/ts/core/search-controller.ts +69 -71
- package/src/ts/models/group-model.ts +21 -21
- package/src/ts/models/option-model.ts +30 -30
- package/src/ts/services/dataset-observer.ts +17 -17
- package/src/ts/services/ea-observer.ts +21 -21
- package/src/ts/services/effector.ts +27 -27
- package/src/ts/services/refresher.ts +1 -1
- package/src/ts/services/resize-observer.ts +29 -29
- package/src/ts/services/select-observer.ts +27 -34
- package/src/ts/types/components/popup.type.ts +15 -0
- package/src/ts/types/utils/istorage.type.ts +1 -1
- package/src/ts/utils/callback-scheduler.ts +41 -21
- package/src/ts/utils/ievents.ts +4 -4
- package/src/ts/utils/istorage.ts +5 -5
- package/src/ts/utils/libs.ts +38 -29
- package/src/ts/utils/selective.ts +11 -11
- package/src/ts/views/group-view.ts +7 -7
- package/src/ts/views/option-view.ts +51 -51
package/dist/selective-ui.umd.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*! Selective UI v1.2.
|
|
1
|
+
/*! Selective UI v1.2.3 | MIT License */
|
|
2
2
|
(function (global, factory) {
|
|
3
3
|
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
|
4
4
|
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
class iStorage {
|
|
12
12
|
constructor() {
|
|
13
13
|
this.defaultConfig = {
|
|
14
|
-
|
|
14
|
+
accessoryVisible: true,
|
|
15
15
|
virtualScroll: true,
|
|
16
16
|
accessoryStyle: "top",
|
|
17
17
|
multiple: false,
|
|
@@ -149,11 +149,14 @@
|
|
|
149
149
|
*/
|
|
150
150
|
run(key, ...params) {
|
|
151
151
|
const executes = this.executeStored.get(key);
|
|
152
|
-
if (!executes)
|
|
153
|
-
return;
|
|
154
|
-
|
|
152
|
+
if (!executes || executes.length === 0) {
|
|
153
|
+
return Promise.resolve();
|
|
154
|
+
}
|
|
155
|
+
if (!this.timerRunner.has(key)) {
|
|
155
156
|
this.timerRunner.set(key, new Map());
|
|
157
|
+
}
|
|
156
158
|
const runner = this.timerRunner.get(key);
|
|
159
|
+
const tasks = [];
|
|
157
160
|
for (let i = 0; i < executes.length; i++) {
|
|
158
161
|
const entry = executes[i];
|
|
159
162
|
if (!entry)
|
|
@@ -161,20 +164,31 @@
|
|
|
161
164
|
const prev = runner.get(i);
|
|
162
165
|
if (prev)
|
|
163
166
|
clearTimeout(prev);
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
167
|
+
const task = new Promise((resolve) => {
|
|
168
|
+
const timer = setTimeout(async () => {
|
|
169
|
+
try {
|
|
170
|
+
const resp = entry.callback(params.length > 0 ? params : null);
|
|
171
|
+
if (resp instanceof Promise) {
|
|
172
|
+
await resp;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
catch { }
|
|
176
|
+
finally {
|
|
177
|
+
if (entry.once) {
|
|
178
|
+
executes[i] = undefined;
|
|
179
|
+
const current = runner.get(i);
|
|
180
|
+
if (current)
|
|
181
|
+
clearTimeout(current);
|
|
182
|
+
runner.delete(i);
|
|
183
|
+
}
|
|
184
|
+
resolve();
|
|
185
|
+
}
|
|
186
|
+
}, entry.timeout);
|
|
187
|
+
runner.set(i, timer);
|
|
188
|
+
});
|
|
189
|
+
tasks.push(task);
|
|
177
190
|
}
|
|
191
|
+
return Promise.all(tasks).then(() => void 0);
|
|
178
192
|
}
|
|
179
193
|
/**
|
|
180
194
|
* Clears callbacks and timers.
|
|
@@ -417,10 +431,20 @@
|
|
|
417
431
|
for (const optionKey in myOptions) {
|
|
418
432
|
const propValue = element[optionKey];
|
|
419
433
|
if (propValue) {
|
|
420
|
-
myOptions[optionKey]
|
|
434
|
+
if (typeof myOptions[optionKey] === "boolean") {
|
|
435
|
+
myOptions[optionKey] = this.string2Boolean(propValue);
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
myOptions[optionKey] = propValue;
|
|
439
|
+
}
|
|
421
440
|
}
|
|
422
441
|
else if (typeof element?.dataset?.[optionKey] !== "undefined") {
|
|
423
|
-
myOptions[optionKey]
|
|
442
|
+
if (typeof myOptions[optionKey] === "boolean") {
|
|
443
|
+
myOptions[optionKey] = this.string2Boolean(element.dataset[optionKey]);
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
myOptions[optionKey] = element.dataset[optionKey];
|
|
447
|
+
}
|
|
424
448
|
}
|
|
425
449
|
}
|
|
426
450
|
return myOptions;
|
|
@@ -821,7 +845,7 @@
|
|
|
821
845
|
*/
|
|
822
846
|
constructor(options) {
|
|
823
847
|
this.node = null;
|
|
824
|
-
this.
|
|
848
|
+
this.options = null;
|
|
825
849
|
if (options)
|
|
826
850
|
this.init(options);
|
|
827
851
|
}
|
|
@@ -836,7 +860,7 @@
|
|
|
836
860
|
classList: "selective-ui-placeholder",
|
|
837
861
|
innerHTML: options.placeholder,
|
|
838
862
|
});
|
|
839
|
-
this.
|
|
863
|
+
this.options = options;
|
|
840
864
|
}
|
|
841
865
|
/**
|
|
842
866
|
* Retrieves the current placeholder text from the configuration.
|
|
@@ -844,7 +868,7 @@
|
|
|
844
868
|
* @returns {string} - The current placeholder text.
|
|
845
869
|
*/
|
|
846
870
|
get() {
|
|
847
|
-
return this.
|
|
871
|
+
return this.options?.placeholder ?? "";
|
|
848
872
|
}
|
|
849
873
|
/**
|
|
850
874
|
* Updates the placeholder text and optionally saves it to the configuration.
|
|
@@ -854,12 +878,12 @@
|
|
|
854
878
|
* @param {boolean} [isSave=true] - Whether to persist the new value in the configuration.
|
|
855
879
|
*/
|
|
856
880
|
set(value, isSave = true) {
|
|
857
|
-
if (!this.node || !this.
|
|
881
|
+
if (!this.node || !this.options)
|
|
858
882
|
return;
|
|
859
883
|
if (isSave)
|
|
860
|
-
this.
|
|
884
|
+
this.options.placeholder = value;
|
|
861
885
|
const translated = Libs.tagTranslate(value);
|
|
862
|
-
this.node.innerHTML = this.
|
|
886
|
+
this.node.innerHTML = this.options.allowHtml ? translated : Libs.stripHtml(translated);
|
|
863
887
|
}
|
|
864
888
|
}
|
|
865
889
|
|
|
@@ -868,13 +892,13 @@
|
|
|
868
892
|
*/
|
|
869
893
|
class Directive {
|
|
870
894
|
constructor() {
|
|
871
|
-
this.node = this.
|
|
895
|
+
this.node = this.init();
|
|
872
896
|
}
|
|
873
897
|
/**
|
|
874
898
|
* Represents a directive button element used to toggle dropdown state.
|
|
875
899
|
* Initializes a clickable node with appropriate ARIA attributes for accessibility.
|
|
876
900
|
*/
|
|
877
|
-
|
|
901
|
+
init() {
|
|
878
902
|
// Libs.nodeCreator returns Element, but this node is always an HTMLElement in practice.
|
|
879
903
|
return Libs.nodeCreator({
|
|
880
904
|
node: "div",
|
|
@@ -906,8 +930,8 @@
|
|
|
906
930
|
this.nodeMounted = null;
|
|
907
931
|
this.node = null;
|
|
908
932
|
this.options = null;
|
|
909
|
-
this.
|
|
910
|
-
this.
|
|
933
|
+
this.actionOnSelectAll = [];
|
|
934
|
+
this.actionOnDeSelectAll = [];
|
|
911
935
|
if (options)
|
|
912
936
|
this.init(options);
|
|
913
937
|
}
|
|
@@ -928,7 +952,7 @@
|
|
|
928
952
|
classList: "selective-ui-option-handle-item",
|
|
929
953
|
textContent: options.textSelectAll,
|
|
930
954
|
onclick: () => {
|
|
931
|
-
iEvents.callFunctions(this.
|
|
955
|
+
iEvents.callFunctions(this.actionOnSelectAll);
|
|
932
956
|
},
|
|
933
957
|
},
|
|
934
958
|
},
|
|
@@ -938,7 +962,7 @@
|
|
|
938
962
|
classList: "selective-ui-option-handle-item",
|
|
939
963
|
textContent: options.textDeselectAll,
|
|
940
964
|
onclick: () => {
|
|
941
|
-
iEvents.callFunctions(this.
|
|
965
|
+
iEvents.callFunctions(this.actionOnDeSelectAll);
|
|
942
966
|
},
|
|
943
967
|
},
|
|
944
968
|
},
|
|
@@ -993,7 +1017,7 @@
|
|
|
993
1017
|
*/
|
|
994
1018
|
OnSelectAll(action = null) {
|
|
995
1019
|
if (typeof action === "function")
|
|
996
|
-
this.
|
|
1020
|
+
this.actionOnSelectAll.push(action);
|
|
997
1021
|
}
|
|
998
1022
|
/**
|
|
999
1023
|
* Registers a callback to be executed when "Deselect All" is clicked.
|
|
@@ -1002,7 +1026,7 @@
|
|
|
1002
1026
|
*/
|
|
1003
1027
|
OnDeSelectAll(action = null) {
|
|
1004
1028
|
if (typeof action === "function")
|
|
1005
|
-
this.
|
|
1029
|
+
this.actionOnDeSelectAll.push(action);
|
|
1006
1030
|
}
|
|
1007
1031
|
}
|
|
1008
1032
|
|
|
@@ -1135,10 +1159,10 @@
|
|
|
1135
1159
|
constructor() {
|
|
1136
1160
|
this.isInit = false;
|
|
1137
1161
|
this.element = null;
|
|
1138
|
-
this.
|
|
1139
|
-
this.
|
|
1162
|
+
this.resizeObserver = null;
|
|
1163
|
+
this.mutationObserver = null;
|
|
1140
1164
|
this.isInit = true;
|
|
1141
|
-
this.
|
|
1165
|
+
this.boundUpdateChanged = this.updateChanged.bind(this);
|
|
1142
1166
|
}
|
|
1143
1167
|
/**
|
|
1144
1168
|
* Callback invoked when the observed element's metrics change.
|
|
@@ -1152,7 +1176,7 @@
|
|
|
1152
1176
|
* Computes the current metrics of the bound element (bounding rect + computed styles)
|
|
1153
1177
|
* and forwards them to `onChanged(metrics)`.
|
|
1154
1178
|
*/
|
|
1155
|
-
|
|
1179
|
+
updateChanged() {
|
|
1156
1180
|
const el = this.element;
|
|
1157
1181
|
if (!el || typeof el.getBoundingClientRect !== "function") {
|
|
1158
1182
|
const defaultMetrics = {
|
|
@@ -1201,7 +1225,7 @@
|
|
|
1201
1225
|
* Manually triggers a metrics computation and notification via `onChanged`.
|
|
1202
1226
|
*/
|
|
1203
1227
|
trigger() {
|
|
1204
|
-
this.
|
|
1228
|
+
this.updateChanged();
|
|
1205
1229
|
}
|
|
1206
1230
|
/**
|
|
1207
1231
|
* Starts observing the provided element for resize and style/class mutations,
|
|
@@ -1215,18 +1239,18 @@
|
|
|
1215
1239
|
throw new Error("Invalid element");
|
|
1216
1240
|
}
|
|
1217
1241
|
this.element = element;
|
|
1218
|
-
this.
|
|
1219
|
-
this.
|
|
1220
|
-
this.
|
|
1221
|
-
this.
|
|
1242
|
+
this.resizeObserver = new ResizeObserver(this.boundUpdateChanged);
|
|
1243
|
+
this.resizeObserver.observe(element);
|
|
1244
|
+
this.mutationObserver = new MutationObserver(this.boundUpdateChanged);
|
|
1245
|
+
this.mutationObserver.observe(element, {
|
|
1222
1246
|
attributes: true,
|
|
1223
1247
|
attributeFilter: ["style", "class"],
|
|
1224
1248
|
});
|
|
1225
|
-
window.addEventListener("scroll", this.
|
|
1226
|
-
window.addEventListener("resize", this.
|
|
1249
|
+
window.addEventListener("scroll", this.boundUpdateChanged, true);
|
|
1250
|
+
window.addEventListener("resize", this.boundUpdateChanged);
|
|
1227
1251
|
if (window.visualViewport) {
|
|
1228
|
-
window.visualViewport.addEventListener("resize", this.
|
|
1229
|
-
window.visualViewport.addEventListener("scroll", this.
|
|
1252
|
+
window.visualViewport.addEventListener("resize", this.boundUpdateChanged);
|
|
1253
|
+
window.visualViewport.addEventListener("scroll", this.boundUpdateChanged);
|
|
1230
1254
|
}
|
|
1231
1255
|
}
|
|
1232
1256
|
/**
|
|
@@ -1234,17 +1258,17 @@
|
|
|
1234
1258
|
* and releases internal observer resources.
|
|
1235
1259
|
*/
|
|
1236
1260
|
disconnect() {
|
|
1237
|
-
this.
|
|
1238
|
-
this.
|
|
1261
|
+
this.resizeObserver?.disconnect();
|
|
1262
|
+
this.mutationObserver?.disconnect();
|
|
1239
1263
|
this.onChanged = (_metrics) => { };
|
|
1240
|
-
window.removeEventListener("scroll", this.
|
|
1241
|
-
window.removeEventListener("resize", this.
|
|
1264
|
+
window.removeEventListener("scroll", this.boundUpdateChanged, true);
|
|
1265
|
+
window.removeEventListener("resize", this.boundUpdateChanged);
|
|
1242
1266
|
if (window.visualViewport) {
|
|
1243
|
-
window.visualViewport.removeEventListener("resize", this.
|
|
1244
|
-
window.visualViewport.removeEventListener("scroll", this.
|
|
1267
|
+
window.visualViewport.removeEventListener("resize", this.boundUpdateChanged);
|
|
1268
|
+
window.visualViewport.removeEventListener("scroll", this.boundUpdateChanged);
|
|
1245
1269
|
}
|
|
1246
|
-
this.
|
|
1247
|
-
this.
|
|
1270
|
+
this.resizeObserver = null;
|
|
1271
|
+
this.mutationObserver = null;
|
|
1248
1272
|
this.element = null;
|
|
1249
1273
|
}
|
|
1250
1274
|
}
|
|
@@ -1266,22 +1290,22 @@
|
|
|
1266
1290
|
this.isCreated = false;
|
|
1267
1291
|
this.optionAdapter = null;
|
|
1268
1292
|
this.node = null;
|
|
1269
|
-
this.
|
|
1270
|
-
this.
|
|
1271
|
-
this.
|
|
1293
|
+
this.effSvc = null;
|
|
1294
|
+
this.resizeObser = null;
|
|
1295
|
+
this.parent = null;
|
|
1272
1296
|
this.optionHandle = null;
|
|
1273
1297
|
this.emptyState = null;
|
|
1274
1298
|
this.loadingState = null;
|
|
1275
1299
|
this.recyclerView = null;
|
|
1276
|
-
this.
|
|
1277
|
-
this.
|
|
1278
|
-
this.
|
|
1300
|
+
this.optionsContainer = null;
|
|
1301
|
+
this.scrollListener = null;
|
|
1302
|
+
this.hideLoadHandle = null;
|
|
1279
1303
|
this.virtualScrollConfig = {
|
|
1280
1304
|
estimateItemHeight: 36,
|
|
1281
1305
|
overscan: 8,
|
|
1282
1306
|
dynamicHeights: true
|
|
1283
1307
|
};
|
|
1284
|
-
this.
|
|
1308
|
+
this.modelManager = modelManager;
|
|
1285
1309
|
if (select && options) {
|
|
1286
1310
|
this.init(select, options);
|
|
1287
1311
|
}
|
|
@@ -1294,7 +1318,7 @@
|
|
|
1294
1318
|
* @param {object} options - Configuration for panel, IDs, multiple mode, and texts.
|
|
1295
1319
|
*/
|
|
1296
1320
|
init(select, options) {
|
|
1297
|
-
if (!this.
|
|
1321
|
+
if (!this.modelManager)
|
|
1298
1322
|
throw new Error("Popup requires a ModelManager instance.");
|
|
1299
1323
|
this.optionHandle = new OptionHandle(options);
|
|
1300
1324
|
this.emptyState = new EmptyState(options);
|
|
@@ -1322,8 +1346,8 @@
|
|
|
1322
1346
|
},
|
|
1323
1347
|
}, null);
|
|
1324
1348
|
this.node = nodeMounted.view;
|
|
1325
|
-
this.
|
|
1326
|
-
this.
|
|
1349
|
+
this.optionsContainer = nodeMounted.tags.OptionsContainer;
|
|
1350
|
+
this.parent = Libs.getBinderMap(select);
|
|
1327
1351
|
this.options = options;
|
|
1328
1352
|
const recyclerViewOpt = options.virtualScroll
|
|
1329
1353
|
? {
|
|
@@ -1334,8 +1358,8 @@
|
|
|
1334
1358
|
}
|
|
1335
1359
|
: {};
|
|
1336
1360
|
// Load ModelManager resources into container
|
|
1337
|
-
this.
|
|
1338
|
-
const MMResources = this.
|
|
1361
|
+
this.modelManager.load(this.optionsContainer, { isMultiple: options.multiple }, recyclerViewOpt);
|
|
1362
|
+
const MMResources = this.modelManager.getResources();
|
|
1339
1363
|
this.optionAdapter = MMResources.adapter;
|
|
1340
1364
|
this.recyclerView = MMResources.recyclerView;
|
|
1341
1365
|
this.optionHandle.OnSelectAll(() => {
|
|
@@ -1344,21 +1368,21 @@
|
|
|
1344
1368
|
this.optionHandle.OnDeSelectAll(() => {
|
|
1345
1369
|
MMResources.adapter.checkAll(false);
|
|
1346
1370
|
});
|
|
1347
|
-
this.
|
|
1371
|
+
this.setupEmptyStateLogic();
|
|
1348
1372
|
}
|
|
1349
1373
|
/**
|
|
1350
1374
|
* Shows the loading state and temporarily skips model events.
|
|
1351
1375
|
* Adjusts size based on current visibility stats and triggers a resize.
|
|
1352
1376
|
*/
|
|
1353
1377
|
async showLoading() {
|
|
1354
|
-
if (!this.options || !this.loadingState || !this.optionHandle || !this.optionAdapter || !this.
|
|
1378
|
+
if (!this.options || !this.loadingState || !this.optionHandle || !this.optionAdapter || !this.modelManager)
|
|
1355
1379
|
return;
|
|
1356
|
-
if (this.
|
|
1357
|
-
clearTimeout(this.
|
|
1358
|
-
this.
|
|
1380
|
+
if (this.hideLoadHandle)
|
|
1381
|
+
clearTimeout(this.hideLoadHandle);
|
|
1382
|
+
this.modelManager.skipEvent(true);
|
|
1359
1383
|
if (Libs.string2Boolean(this.options.loadingfield) === false)
|
|
1360
1384
|
return;
|
|
1361
|
-
// this.
|
|
1385
|
+
// this.updateEmptyState({isEmpty: false, hasVisible: true});
|
|
1362
1386
|
this.emptyState.hide();
|
|
1363
1387
|
this.loadingState.show(this.optionAdapter.getVisibilityStats().hasVisible);
|
|
1364
1388
|
// this.optionHandle.hide();
|
|
@@ -1369,31 +1393,31 @@
|
|
|
1369
1393
|
* updates empty state based on adapter visibility stats, and triggers a resize.
|
|
1370
1394
|
*/
|
|
1371
1395
|
async hideLoading() {
|
|
1372
|
-
if (!this.options || !this.loadingState || !this.optionAdapter || !this.
|
|
1396
|
+
if (!this.options || !this.loadingState || !this.optionAdapter || !this.modelManager)
|
|
1373
1397
|
return;
|
|
1374
|
-
if (this.
|
|
1375
|
-
clearTimeout(this.
|
|
1376
|
-
this.
|
|
1377
|
-
this.
|
|
1398
|
+
if (this.hideLoadHandle)
|
|
1399
|
+
clearTimeout(this.hideLoadHandle);
|
|
1400
|
+
this.hideLoadHandle = setTimeout(() => {
|
|
1401
|
+
this.modelManager?.skipEvent(false);
|
|
1378
1402
|
this.loadingState?.hide();
|
|
1379
1403
|
const stats = this.optionAdapter?.getVisibilityStats();
|
|
1380
|
-
this.
|
|
1404
|
+
this.updateEmptyState(stats ?? undefined);
|
|
1381
1405
|
this.triggerResize();
|
|
1382
|
-
},
|
|
1406
|
+
}, this.options.animationtime);
|
|
1383
1407
|
}
|
|
1384
1408
|
/**
|
|
1385
1409
|
* Subscribes to adapter visibility and item changes to keep the empty state in sync.
|
|
1386
1410
|
* Triggers resize when items change to reflect layout updates.
|
|
1387
1411
|
*/
|
|
1388
|
-
|
|
1412
|
+
setupEmptyStateLogic() {
|
|
1389
1413
|
if (!this.optionAdapter)
|
|
1390
1414
|
return;
|
|
1391
1415
|
this.optionAdapter.onVisibilityChanged((stats) => {
|
|
1392
|
-
this.
|
|
1416
|
+
this.updateEmptyState(stats);
|
|
1393
1417
|
});
|
|
1394
1418
|
this.optionAdapter.onPropChanged("items", () => {
|
|
1395
1419
|
const stats = this.optionAdapter.getVisibilityStats();
|
|
1396
|
-
this.
|
|
1420
|
+
this.updateEmptyState(stats);
|
|
1397
1421
|
this.triggerResize();
|
|
1398
1422
|
});
|
|
1399
1423
|
}
|
|
@@ -1403,23 +1427,23 @@
|
|
|
1403
1427
|
*
|
|
1404
1428
|
* @param {VisibilityStats|undefined} stats - Visibility stats; computed if omitted.
|
|
1405
1429
|
*/
|
|
1406
|
-
|
|
1407
|
-
if (!this.optionAdapter || !this.emptyState || !this.optionHandle || !this.
|
|
1430
|
+
updateEmptyState(stats) {
|
|
1431
|
+
if (!this.optionAdapter || !this.emptyState || !this.optionHandle || !this.optionsContainer)
|
|
1408
1432
|
return;
|
|
1409
1433
|
const s = stats ?? this.optionAdapter.getVisibilityStats();
|
|
1410
1434
|
if (s.isEmpty) {
|
|
1411
1435
|
this.emptyState.show("nodata");
|
|
1412
|
-
this.
|
|
1436
|
+
this.optionsContainer.classList.add("hide");
|
|
1413
1437
|
this.optionHandle.hide();
|
|
1414
1438
|
}
|
|
1415
1439
|
else if (!s.hasVisible) {
|
|
1416
1440
|
this.emptyState.show("notfound");
|
|
1417
|
-
this.
|
|
1441
|
+
this.optionsContainer.classList.add("hide");
|
|
1418
1442
|
this.optionHandle.hide();
|
|
1419
1443
|
}
|
|
1420
1444
|
else {
|
|
1421
1445
|
this.emptyState.hide();
|
|
1422
|
-
this.
|
|
1446
|
+
this.optionsContainer.classList.remove("hide");
|
|
1423
1447
|
this.optionHandle.refresh();
|
|
1424
1448
|
}
|
|
1425
1449
|
}
|
|
@@ -1439,20 +1463,20 @@
|
|
|
1439
1463
|
* Injects an effector service used to perform side effects (e.g., animations or external actions).
|
|
1440
1464
|
*/
|
|
1441
1465
|
setupEffector(effectorSvc) {
|
|
1442
|
-
this.
|
|
1466
|
+
this.effSvc = effectorSvc;
|
|
1443
1467
|
}
|
|
1444
1468
|
/**
|
|
1445
1469
|
* Opens the popup: creates and attaches DOM if needed, initializes observers and effector,
|
|
1446
1470
|
* computes position and dimensions, and runs expand animation. Invokes callback on completion.
|
|
1447
1471
|
*/
|
|
1448
1472
|
open(callback = null, isShowEmptyState) {
|
|
1449
|
-
if (!this.node || !this.options || !this.optionHandle || !this.
|
|
1473
|
+
if (!this.node || !this.options || !this.optionHandle || !this.parent || !this.effSvc)
|
|
1450
1474
|
return;
|
|
1451
1475
|
if (!this.isCreated) {
|
|
1452
1476
|
document.body.appendChild(this.node);
|
|
1453
1477
|
this.isCreated = true;
|
|
1454
|
-
this.
|
|
1455
|
-
this.
|
|
1478
|
+
this.resizeObser = new ResizeObserverService();
|
|
1479
|
+
this.effSvc.setElement(this.node);
|
|
1456
1480
|
this.node.addEventListener("mousedown", (e) => {
|
|
1457
1481
|
e.stopPropagation();
|
|
1458
1482
|
e.preventDefault();
|
|
@@ -1460,11 +1484,11 @@
|
|
|
1460
1484
|
}
|
|
1461
1485
|
this.optionHandle.refresh();
|
|
1462
1486
|
if (isShowEmptyState) {
|
|
1463
|
-
this.
|
|
1487
|
+
this.updateEmptyState();
|
|
1464
1488
|
}
|
|
1465
|
-
const location = this.
|
|
1466
|
-
const { position, top, maxHeight, realHeight } = this.
|
|
1467
|
-
this.
|
|
1489
|
+
const location = this.getParentLocation();
|
|
1490
|
+
const { position, top, maxHeight, realHeight } = this.calculatePosition(location);
|
|
1491
|
+
this.effSvc.expand({
|
|
1468
1492
|
duration: this.options.animationtime,
|
|
1469
1493
|
display: "flex",
|
|
1470
1494
|
width: location.width,
|
|
@@ -1474,14 +1498,14 @@
|
|
|
1474
1498
|
realHeight,
|
|
1475
1499
|
position,
|
|
1476
1500
|
onComplete: () => {
|
|
1477
|
-
if (!this.
|
|
1501
|
+
if (!this.resizeObser || !this.parent)
|
|
1478
1502
|
return;
|
|
1479
|
-
this.
|
|
1503
|
+
this.resizeObser.onChanged = (_metrics) => {
|
|
1480
1504
|
// Recompute from parent each time to keep behavior identical.
|
|
1481
|
-
const loc = this.
|
|
1482
|
-
this.
|
|
1505
|
+
const loc = this.getParentLocation();
|
|
1506
|
+
this.handleResize(loc);
|
|
1483
1507
|
};
|
|
1484
|
-
this.
|
|
1508
|
+
this.resizeObser.connect(this.parent.container.tags.ViewPanel);
|
|
1485
1509
|
callback?.();
|
|
1486
1510
|
const rv = this.recyclerView;
|
|
1487
1511
|
rv?.resume?.();
|
|
@@ -1493,12 +1517,12 @@
|
|
|
1493
1517
|
* Safely no-ops if the popup has not been created.
|
|
1494
1518
|
*/
|
|
1495
1519
|
close(callback = null) {
|
|
1496
|
-
if (!this.isCreated || !this.options || !this.
|
|
1520
|
+
if (!this.isCreated || !this.options || !this.resizeObser || !this.effSvc)
|
|
1497
1521
|
return;
|
|
1498
1522
|
const rv = this.recyclerView;
|
|
1499
1523
|
rv?.suspend?.();
|
|
1500
|
-
this.
|
|
1501
|
-
this.
|
|
1524
|
+
this.resizeObser.disconnect();
|
|
1525
|
+
this.effSvc.collapse({
|
|
1502
1526
|
duration: this.options.animationtime,
|
|
1503
1527
|
onComplete: callback ?? undefined,
|
|
1504
1528
|
});
|
|
@@ -1509,7 +1533,7 @@
|
|
|
1509
1533
|
*/
|
|
1510
1534
|
triggerResize() {
|
|
1511
1535
|
if (this.isCreated)
|
|
1512
|
-
this.
|
|
1536
|
+
this.resizeObser?.trigger();
|
|
1513
1537
|
}
|
|
1514
1538
|
/**
|
|
1515
1539
|
* Enables infinite scroll by listening to container scroll events and loading more data
|
|
@@ -1521,7 +1545,7 @@
|
|
|
1521
1545
|
setupInfiniteScroll(searchController, _options) {
|
|
1522
1546
|
if (!this.node)
|
|
1523
1547
|
return;
|
|
1524
|
-
this.
|
|
1548
|
+
this.scrollListener = async () => {
|
|
1525
1549
|
const state = searchController.getPaginationState();
|
|
1526
1550
|
if (!state.isPaginationEnabled)
|
|
1527
1551
|
return;
|
|
@@ -1539,7 +1563,7 @@
|
|
|
1539
1563
|
}
|
|
1540
1564
|
}
|
|
1541
1565
|
};
|
|
1542
|
-
this.node.addEventListener("scroll", this.
|
|
1566
|
+
this.node.addEventListener("scroll", this.scrollListener);
|
|
1543
1567
|
}
|
|
1544
1568
|
/**
|
|
1545
1569
|
* Completely tear down the popup instance and release all resources.
|
|
@@ -1554,24 +1578,24 @@
|
|
|
1554
1578
|
* Safe to call multiple times; all operations are guarded via optional chaining.
|
|
1555
1579
|
*/
|
|
1556
1580
|
detroy() {
|
|
1557
|
-
if (this.
|
|
1558
|
-
clearTimeout(this.
|
|
1559
|
-
this.
|
|
1581
|
+
if (this.hideLoadHandle) {
|
|
1582
|
+
clearTimeout(this.hideLoadHandle);
|
|
1583
|
+
this.hideLoadHandle = null;
|
|
1560
1584
|
}
|
|
1561
|
-
if (this.node && this.
|
|
1562
|
-
this.node.removeEventListener("scroll", this.
|
|
1563
|
-
this.
|
|
1585
|
+
if (this.node && this.scrollListener) {
|
|
1586
|
+
this.node.removeEventListener("scroll", this.scrollListener);
|
|
1587
|
+
this.scrollListener = null;
|
|
1564
1588
|
}
|
|
1565
1589
|
try {
|
|
1566
|
-
this.
|
|
1590
|
+
this.resizeObser?.disconnect();
|
|
1567
1591
|
}
|
|
1568
1592
|
catch (_) { }
|
|
1569
|
-
this.
|
|
1593
|
+
this.resizeObser = null;
|
|
1570
1594
|
try {
|
|
1571
|
-
this.
|
|
1595
|
+
this.effSvc?.setElement?.(null);
|
|
1572
1596
|
}
|
|
1573
1597
|
catch (_) { }
|
|
1574
|
-
this.
|
|
1598
|
+
this.effSvc = null;
|
|
1575
1599
|
if (this.node) {
|
|
1576
1600
|
try {
|
|
1577
1601
|
const clone = this.node.cloneNode(true);
|
|
@@ -1583,20 +1607,20 @@
|
|
|
1583
1607
|
}
|
|
1584
1608
|
}
|
|
1585
1609
|
this.node = null;
|
|
1586
|
-
this.
|
|
1610
|
+
this.optionsContainer = null;
|
|
1587
1611
|
try {
|
|
1588
|
-
this.
|
|
1612
|
+
this.modelManager?.skipEvent?.(false);
|
|
1589
1613
|
this.recyclerView?.clear?.();
|
|
1590
1614
|
this.recyclerView = null;
|
|
1591
1615
|
this.optionAdapter = null;
|
|
1592
1616
|
this.node.remove();
|
|
1593
1617
|
}
|
|
1594
1618
|
catch (_) { }
|
|
1595
|
-
this.
|
|
1619
|
+
this.modelManager = null;
|
|
1596
1620
|
this.optionHandle = null;
|
|
1597
1621
|
this.emptyState = null;
|
|
1598
1622
|
this.loadingState = null;
|
|
1599
|
-
this.
|
|
1623
|
+
this.parent = null;
|
|
1600
1624
|
this.options = null;
|
|
1601
1625
|
this.isCreated = false;
|
|
1602
1626
|
}
|
|
@@ -1604,8 +1628,8 @@
|
|
|
1604
1628
|
* Computes the parent panel's location and box metrics, including size, position,
|
|
1605
1629
|
* padding, and border, accounting for iOS visual viewport offsets.
|
|
1606
1630
|
*/
|
|
1607
|
-
|
|
1608
|
-
const viewPanel = this.
|
|
1631
|
+
getParentLocation() {
|
|
1632
|
+
const viewPanel = this.parent.container.tags.ViewPanel;
|
|
1609
1633
|
const rect = viewPanel.getBoundingClientRect();
|
|
1610
1634
|
const style = window.getComputedStyle(viewPanel);
|
|
1611
1635
|
return {
|
|
@@ -1631,13 +1655,13 @@
|
|
|
1631
1655
|
* Determines popup placement (top/bottom) and height constraints based on available viewport space,
|
|
1632
1656
|
* content size, and configured min/max heights; returns final position, top, and heights.
|
|
1633
1657
|
*/
|
|
1634
|
-
|
|
1658
|
+
calculatePosition(location) {
|
|
1635
1659
|
const vv = window.visualViewport;
|
|
1636
1660
|
const is_ios = Libs.IsIOS();
|
|
1637
1661
|
const viewportHeight = vv?.height ?? window.innerHeight;
|
|
1638
1662
|
const gap = 3;
|
|
1639
1663
|
const safeMargin = 15;
|
|
1640
|
-
const dimensions = this.
|
|
1664
|
+
const dimensions = this.effSvc.getHiddenDimensions("flex");
|
|
1641
1665
|
const contentHeight = dimensions.scrollHeight;
|
|
1642
1666
|
const configMaxHeight = parseFloat(this.options?.panelHeight ?? "220") || 220;
|
|
1643
1667
|
const configMinHeight = parseFloat(this.options?.panelMinHeight ?? "100") || 100;
|
|
@@ -1676,11 +1700,11 @@
|
|
|
1676
1700
|
* Handles parent resize events by recalculating placement and dimensions,
|
|
1677
1701
|
* then animates the popup to the new size and position.
|
|
1678
1702
|
*/
|
|
1679
|
-
|
|
1680
|
-
if (!this.options || !this.
|
|
1703
|
+
handleResize(location) {
|
|
1704
|
+
if (!this.options || !this.effSvc)
|
|
1681
1705
|
return;
|
|
1682
|
-
const { position, top, maxHeight, realHeight } = this.
|
|
1683
|
-
this.
|
|
1706
|
+
const { position, top, maxHeight, realHeight } = this.calculatePosition(location);
|
|
1707
|
+
this.effSvc.resize({
|
|
1684
1708
|
duration: this.options.animationtime,
|
|
1685
1709
|
width: location.width,
|
|
1686
1710
|
left: location.left,
|
|
@@ -1854,8 +1878,8 @@
|
|
|
1854
1878
|
* @param {string|HTMLElement|null} [query] - A CSS selector or the target element to control.
|
|
1855
1879
|
*/
|
|
1856
1880
|
constructor(query = null) {
|
|
1857
|
-
this.
|
|
1858
|
-
this.
|
|
1881
|
+
this.timeOut = null;
|
|
1882
|
+
this.resizeTimeout = null;
|
|
1859
1883
|
this._isAnimating = false;
|
|
1860
1884
|
if (query)
|
|
1861
1885
|
this.setElement(query);
|
|
@@ -1882,13 +1906,13 @@
|
|
|
1882
1906
|
* @returns {this} - The effector instance for chaining.
|
|
1883
1907
|
*/
|
|
1884
1908
|
cancel() {
|
|
1885
|
-
if (this.
|
|
1886
|
-
clearTimeout(this.
|
|
1887
|
-
this.
|
|
1909
|
+
if (this.timeOut) {
|
|
1910
|
+
clearTimeout(this.timeOut);
|
|
1911
|
+
this.timeOut = null;
|
|
1888
1912
|
}
|
|
1889
|
-
if (this.
|
|
1890
|
-
clearTimeout(this.
|
|
1891
|
-
this.
|
|
1913
|
+
if (this.resizeTimeout) {
|
|
1914
|
+
clearTimeout(this.resizeTimeout);
|
|
1915
|
+
this.resizeTimeout = null;
|
|
1892
1916
|
}
|
|
1893
1917
|
this._isAnimating = false;
|
|
1894
1918
|
return this;
|
|
@@ -1963,7 +1987,7 @@
|
|
|
1963
1987
|
opacity: "1",
|
|
1964
1988
|
overflow: isScrollable ? "auto" : "hidden",
|
|
1965
1989
|
});
|
|
1966
|
-
this.
|
|
1990
|
+
this.timeOut = setTimeout(() => {
|
|
1967
1991
|
this.element.style.transition = "none";
|
|
1968
1992
|
this._isAnimating = false;
|
|
1969
1993
|
onComplete?.();
|
|
@@ -1995,7 +2019,7 @@
|
|
|
1995
2019
|
opacity: "0",
|
|
1996
2020
|
overflow: isScrollable ? "auto" : "hidden",
|
|
1997
2021
|
});
|
|
1998
|
-
this.
|
|
2022
|
+
this.timeOut = setTimeout(() => {
|
|
1999
2023
|
Object.assign(this.element.style, {
|
|
2000
2024
|
display: "none",
|
|
2001
2025
|
transition: "none",
|
|
@@ -2031,7 +2055,7 @@
|
|
|
2031
2055
|
overflow: "hidden",
|
|
2032
2056
|
});
|
|
2033
2057
|
});
|
|
2034
|
-
this.
|
|
2058
|
+
this.timeOut = setTimeout(() => {
|
|
2035
2059
|
Object.assign(this.element.style, {
|
|
2036
2060
|
width: "",
|
|
2037
2061
|
overflow: "",
|
|
@@ -2065,7 +2089,7 @@
|
|
|
2065
2089
|
overflow: "hidden",
|
|
2066
2090
|
});
|
|
2067
2091
|
});
|
|
2068
|
-
this.
|
|
2092
|
+
this.timeOut = setTimeout(() => {
|
|
2069
2093
|
Object.assign(this.element.style, {
|
|
2070
2094
|
width: "",
|
|
2071
2095
|
overflow: "",
|
|
@@ -2095,7 +2119,7 @@
|
|
|
2095
2119
|
if (isPositionChanged) {
|
|
2096
2120
|
this.element.style.transition = `top ${duration}ms ease-out, height ${duration}ms ease-out, max-height ${duration}ms ease-out;`;
|
|
2097
2121
|
}
|
|
2098
|
-
|
|
2122
|
+
requestAnimationFrame(() => {
|
|
2099
2123
|
const styles = {
|
|
2100
2124
|
width: `${width}px`,
|
|
2101
2125
|
left: `${left}px`,
|
|
@@ -2109,7 +2133,7 @@
|
|
|
2109
2133
|
styles.transition = `height ${duration}ms, top ${duration}ms`;
|
|
2110
2134
|
}
|
|
2111
2135
|
else {
|
|
2112
|
-
this.
|
|
2136
|
+
this.resizeTimeout = setTimeout(() => {
|
|
2113
2137
|
if (this.element?.style) {
|
|
2114
2138
|
this.element.style.transition = null;
|
|
2115
2139
|
}
|
|
@@ -2117,7 +2141,7 @@
|
|
|
2117
2141
|
}
|
|
2118
2142
|
Object.assign(this.element.style, styles);
|
|
2119
2143
|
if (animate && (isPositionChanged || heightDiff > 1)) {
|
|
2120
|
-
this.
|
|
2144
|
+
this.resizeTimeout = setTimeout(() => {
|
|
2121
2145
|
this.element.style.transition = null;
|
|
2122
2146
|
if (isPositionChanged)
|
|
2123
2147
|
delete this.element.style.transition;
|
|
@@ -2129,7 +2153,7 @@
|
|
|
2129
2153
|
delete this.element.style.transition;
|
|
2130
2154
|
onComplete?.();
|
|
2131
2155
|
}
|
|
2132
|
-
}
|
|
2156
|
+
});
|
|
2133
2157
|
return this;
|
|
2134
2158
|
}
|
|
2135
2159
|
/**
|
|
@@ -2212,7 +2236,7 @@
|
|
|
2212
2236
|
* Initializes a group model with options and an optional <optgroup> target.
|
|
2213
2237
|
* Reads the label and collapsed state from the target element's attributes/dataset.
|
|
2214
2238
|
*
|
|
2215
|
-
* @param {
|
|
2239
|
+
* @param {SelectiveOptions} options - Configuration for the model.
|
|
2216
2240
|
* @param {HTMLOptGroupElement} [targetElement] - The source <optgroup> element.
|
|
2217
2241
|
*/
|
|
2218
2242
|
constructor(options, targetElement) {
|
|
@@ -2220,7 +2244,7 @@
|
|
|
2220
2244
|
this.label = "";
|
|
2221
2245
|
this.items = [];
|
|
2222
2246
|
this.collapsed = false;
|
|
2223
|
-
this.
|
|
2247
|
+
this.privOnCollapsedChanged = [];
|
|
2224
2248
|
if (targetElement) {
|
|
2225
2249
|
this.label = targetElement.label;
|
|
2226
2250
|
this.collapsed = Libs.string2Boolean(targetElement.dataset?.collapsed);
|
|
@@ -2283,7 +2307,7 @@
|
|
|
2283
2307
|
* @param {(evtToken: IEventCallback, model: GroupModel, collapsed: boolean) => void} callback - Listener for collapse changes.
|
|
2284
2308
|
*/
|
|
2285
2309
|
onCollapsedChanged(callback) {
|
|
2286
|
-
this.
|
|
2310
|
+
this.privOnCollapsedChanged.push(callback);
|
|
2287
2311
|
}
|
|
2288
2312
|
/**
|
|
2289
2313
|
* Toggles the group's collapsed state, updates the view, and notifies registered listeners.
|
|
@@ -2291,7 +2315,7 @@
|
|
|
2291
2315
|
toggleCollapse() {
|
|
2292
2316
|
this.collapsed = !this.collapsed;
|
|
2293
2317
|
this.view?.setCollapsed(this.collapsed);
|
|
2294
|
-
iEvents.callEvent([this, this.collapsed], ...this.
|
|
2318
|
+
iEvents.callEvent([this, this.collapsed], ...this.privOnCollapsedChanged);
|
|
2295
2319
|
}
|
|
2296
2320
|
/**
|
|
2297
2321
|
* Adds an option item to this group and sets its back-reference to the group.
|
|
@@ -2337,9 +2361,9 @@
|
|
|
2337
2361
|
*/
|
|
2338
2362
|
constructor(options, targetElement = null, view = null) {
|
|
2339
2363
|
super(options, targetElement, view);
|
|
2340
|
-
this.
|
|
2341
|
-
this.
|
|
2342
|
-
this.
|
|
2364
|
+
this.privOnSelected = [];
|
|
2365
|
+
this.privOnInternalSelected = [];
|
|
2366
|
+
this.privOnVisibilityChanged = [];
|
|
2343
2367
|
this._visible = true;
|
|
2344
2368
|
this._highlighted = false;
|
|
2345
2369
|
this.group = null;
|
|
@@ -2387,7 +2411,7 @@
|
|
|
2387
2411
|
*/
|
|
2388
2412
|
set selected(value) {
|
|
2389
2413
|
this.selectedNonTrigger = value;
|
|
2390
|
-
iEvents.callEvent([this, value], ...this.
|
|
2414
|
+
iEvents.callEvent([this, value], ...this.privOnSelected);
|
|
2391
2415
|
}
|
|
2392
2416
|
/**
|
|
2393
2417
|
* Gets whether the option is currently visible in the UI.
|
|
@@ -2409,7 +2433,7 @@
|
|
|
2409
2433
|
const viewEl = this.view?.getView?.();
|
|
2410
2434
|
if (viewEl)
|
|
2411
2435
|
viewEl.classList.toggle("hide", !value);
|
|
2412
|
-
iEvents.callEvent([this, value], ...this.
|
|
2436
|
+
iEvents.callEvent([this, value], ...this.privOnVisibilityChanged);
|
|
2413
2437
|
}
|
|
2414
2438
|
/**
|
|
2415
2439
|
* Gets the selected state without triggering external listeners (alias of selected).
|
|
@@ -2437,7 +2461,7 @@
|
|
|
2437
2461
|
}
|
|
2438
2462
|
if (this.targetElement)
|
|
2439
2463
|
this.targetElement.selected = value;
|
|
2440
|
-
iEvents.callEvent([this, value], ...this.
|
|
2464
|
+
iEvents.callEvent([this, value], ...this.privOnInternalSelected);
|
|
2441
2465
|
}
|
|
2442
2466
|
/**
|
|
2443
2467
|
* Returns the display text for the option, applying tag translation and optional HTML allowance.
|
|
@@ -2495,7 +2519,7 @@
|
|
|
2495
2519
|
* @param {(evtToken: IEventCallback, el: OptionModel, selected: boolean) => void} callback - Selection listener.
|
|
2496
2520
|
*/
|
|
2497
2521
|
onSelected(callback) {
|
|
2498
|
-
this.
|
|
2522
|
+
this.privOnSelected.push(callback);
|
|
2499
2523
|
}
|
|
2500
2524
|
/**
|
|
2501
2525
|
* Registers a listener invoked when internal selection changes (via setter `selectedNonTrigger`).
|
|
@@ -2503,7 +2527,7 @@
|
|
|
2503
2527
|
* @param {(evtToken: IEventCallback, el: OptionModel, selected: boolean) => void} callback - Internal selection listener.
|
|
2504
2528
|
*/
|
|
2505
2529
|
onInternalSelected(callback) {
|
|
2506
|
-
this.
|
|
2530
|
+
this.privOnInternalSelected.push(callback);
|
|
2507
2531
|
}
|
|
2508
2532
|
/**
|
|
2509
2533
|
* Registers a listener invoked when visibility changes (via setter `visible`).
|
|
@@ -2511,7 +2535,7 @@
|
|
|
2511
2535
|
* @param {(evtToken: IEventCallback, model: OptionModel, visible: boolean) => void} callback - Visibility listener.
|
|
2512
2536
|
*/
|
|
2513
2537
|
onVisibilityChanged(callback) {
|
|
2514
|
-
this.
|
|
2538
|
+
this.privOnVisibilityChanged.push(callback);
|
|
2515
2539
|
}
|
|
2516
2540
|
/**
|
|
2517
2541
|
* Hook called when the target <option> element changes.
|
|
@@ -2552,11 +2576,12 @@
|
|
|
2552
2576
|
* @param {object} options - Configuration object passed to GroupModel/OptionModel and view infrastructure.
|
|
2553
2577
|
*/
|
|
2554
2578
|
constructor(options) {
|
|
2555
|
-
this.
|
|
2556
|
-
this.
|
|
2557
|
-
this.
|
|
2558
|
-
this.
|
|
2579
|
+
this.privModelList = [];
|
|
2580
|
+
this.privAdapterHandle = null;
|
|
2581
|
+
this.privRecyclerViewHandle = null;
|
|
2582
|
+
this.lastFingerprint = null;
|
|
2559
2583
|
this.options = null;
|
|
2584
|
+
this.oldPosition = 0;
|
|
2560
2585
|
this.options = options;
|
|
2561
2586
|
}
|
|
2562
2587
|
/**
|
|
@@ -2565,7 +2590,7 @@
|
|
|
2565
2590
|
* @param {new TAdapter} adapter - The adapter constructor (class) to instantiate.
|
|
2566
2591
|
*/
|
|
2567
2592
|
setupAdapter(adapter) {
|
|
2568
|
-
this.
|
|
2593
|
+
this.privAdapter = adapter;
|
|
2569
2594
|
}
|
|
2570
2595
|
/**
|
|
2571
2596
|
* Registers the RecyclerView class responsible for hosting and updating item views.
|
|
@@ -2573,7 +2598,7 @@
|
|
|
2573
2598
|
* @param {new RecyclerViewContract<TAdapter>} recyclerView - The recycler view constructor.
|
|
2574
2599
|
*/
|
|
2575
2600
|
setupRecyclerView(recyclerView) {
|
|
2576
|
-
this.
|
|
2601
|
+
this.privRecyclerView = recyclerView;
|
|
2577
2602
|
}
|
|
2578
2603
|
/**
|
|
2579
2604
|
* Checks whether the provided model data differs from the last recorded fingerprint.
|
|
@@ -2584,10 +2609,10 @@
|
|
|
2584
2609
|
* @returns {boolean} True if there are real changes; false otherwise.
|
|
2585
2610
|
*/
|
|
2586
2611
|
hasRealChanges(modelData) {
|
|
2587
|
-
const newFingerprint = this.
|
|
2588
|
-
const hasChanges = newFingerprint !== this.
|
|
2612
|
+
const newFingerprint = this.createFingerprint(modelData);
|
|
2613
|
+
const hasChanges = newFingerprint !== this.lastFingerprint;
|
|
2589
2614
|
if (hasChanges)
|
|
2590
|
-
this.
|
|
2615
|
+
this.lastFingerprint = newFingerprint;
|
|
2591
2616
|
return hasChanges;
|
|
2592
2617
|
}
|
|
2593
2618
|
/**
|
|
@@ -2599,7 +2624,7 @@
|
|
|
2599
2624
|
* @param {Array<HTMLOptionElement|HTMLOptGroupElement>} modelData - The current model data to fingerprint.
|
|
2600
2625
|
* @returns {string} A deterministic fingerprint representing the structure and selection state.
|
|
2601
2626
|
*/
|
|
2602
|
-
|
|
2627
|
+
createFingerprint(modelData) {
|
|
2603
2628
|
return modelData
|
|
2604
2629
|
.map((item) => {
|
|
2605
2630
|
if (item.tagName === "OPTGROUP") {
|
|
@@ -2627,12 +2652,12 @@
|
|
|
2627
2652
|
* @returns {Array<GroupModel|OptionModel>} - The ordered list of group and option models.
|
|
2628
2653
|
*/
|
|
2629
2654
|
createModelResources(modelData) {
|
|
2630
|
-
this.
|
|
2655
|
+
this.privModelList = [];
|
|
2631
2656
|
let currentGroup = null;
|
|
2632
2657
|
modelData.forEach((data) => {
|
|
2633
2658
|
if (data.tagName === "OPTGROUP") {
|
|
2634
2659
|
currentGroup = new GroupModel(this.options, data);
|
|
2635
|
-
this.
|
|
2660
|
+
this.privModelList.push(currentGroup);
|
|
2636
2661
|
}
|
|
2637
2662
|
else if (data.tagName === "OPTION") {
|
|
2638
2663
|
const optionEl = data;
|
|
@@ -2643,12 +2668,12 @@
|
|
|
2643
2668
|
optionModel.group = currentGroup;
|
|
2644
2669
|
}
|
|
2645
2670
|
else {
|
|
2646
|
-
this.
|
|
2671
|
+
this.privModelList.push(optionModel);
|
|
2647
2672
|
currentGroup = null;
|
|
2648
2673
|
}
|
|
2649
2674
|
}
|
|
2650
2675
|
});
|
|
2651
|
-
return this.
|
|
2676
|
+
return this.privModelList;
|
|
2652
2677
|
}
|
|
2653
2678
|
/**
|
|
2654
2679
|
* Replaces the current model list with new data and syncs it into the adapter,
|
|
@@ -2656,12 +2681,12 @@
|
|
|
2656
2681
|
*
|
|
2657
2682
|
* @param {Array<HTMLOptGroupElement|HTMLOptionElement>} modelData - New source elements to rebuild models from.
|
|
2658
2683
|
*/
|
|
2659
|
-
replace(modelData) {
|
|
2660
|
-
this.
|
|
2684
|
+
async replace(modelData) {
|
|
2685
|
+
this.lastFingerprint = null;
|
|
2661
2686
|
this.createModelResources(modelData);
|
|
2662
|
-
if (this.
|
|
2687
|
+
if (this.privAdapterHandle) {
|
|
2663
2688
|
// Adapter expects TModel[], but this manager's list is GroupModel|OptionModel.
|
|
2664
|
-
this.
|
|
2689
|
+
await this.privAdapterHandle.syncFromSource(this.privModelList);
|
|
2665
2690
|
}
|
|
2666
2691
|
this.refresh(false);
|
|
2667
2692
|
}
|
|
@@ -2670,7 +2695,7 @@
|
|
|
2670
2695
|
* typically used after external updates to model data.
|
|
2671
2696
|
*/
|
|
2672
2697
|
notify() {
|
|
2673
|
-
if (!this.
|
|
2698
|
+
if (!this.privAdapterHandle)
|
|
2674
2699
|
return;
|
|
2675
2700
|
this.refresh(false);
|
|
2676
2701
|
}
|
|
@@ -2679,11 +2704,11 @@
|
|
|
2679
2704
|
* and applies optional configuration overrides for adapter and recyclerView.
|
|
2680
2705
|
*/
|
|
2681
2706
|
load(viewElement, adapterOpt = {}, recyclerViewOpt = {}) {
|
|
2682
|
-
this.
|
|
2683
|
-
Object.assign(this.
|
|
2684
|
-
this.
|
|
2685
|
-
Object.assign(this.
|
|
2686
|
-
this.
|
|
2707
|
+
this.privAdapterHandle = new this.privAdapter(this.privModelList);
|
|
2708
|
+
Object.assign(this.privAdapterHandle, adapterOpt);
|
|
2709
|
+
this.privRecyclerViewHandle = new this.privRecyclerView(viewElement);
|
|
2710
|
+
Object.assign(this.privRecyclerViewHandle, recyclerViewOpt);
|
|
2711
|
+
this.privRecyclerViewHandle.setAdapter(this.privAdapterHandle);
|
|
2687
2712
|
}
|
|
2688
2713
|
/**
|
|
2689
2714
|
* Diffs existing models against new <optgroup>/<option> data to update in place:
|
|
@@ -2693,7 +2718,7 @@
|
|
|
2693
2718
|
update(modelData) {
|
|
2694
2719
|
if (!this.hasRealChanges(modelData))
|
|
2695
2720
|
return;
|
|
2696
|
-
const oldModels = this.
|
|
2721
|
+
const oldModels = this.privModelList;
|
|
2697
2722
|
const newModels = [];
|
|
2698
2723
|
const oldGroupMap = new Map();
|
|
2699
2724
|
const oldOptionMap = new Map();
|
|
@@ -2765,6 +2790,10 @@
|
|
|
2765
2790
|
}
|
|
2766
2791
|
});
|
|
2767
2792
|
let isUpdate = true;
|
|
2793
|
+
if (this.oldPosition == 0) {
|
|
2794
|
+
isUpdate = false;
|
|
2795
|
+
}
|
|
2796
|
+
this.oldPosition = position;
|
|
2768
2797
|
oldGroupMap.forEach((removedGroup) => {
|
|
2769
2798
|
isUpdate = false;
|
|
2770
2799
|
removedGroup.remove();
|
|
@@ -2773,11 +2802,11 @@
|
|
|
2773
2802
|
isUpdate = false;
|
|
2774
2803
|
removedOption.remove();
|
|
2775
2804
|
});
|
|
2776
|
-
this.
|
|
2777
|
-
if (this.
|
|
2778
|
-
this.
|
|
2805
|
+
this.privModelList = newModels;
|
|
2806
|
+
if (this.privAdapterHandle) {
|
|
2807
|
+
this.privAdapterHandle.updateData(this.privModelList);
|
|
2779
2808
|
}
|
|
2780
|
-
this.onUpdated();
|
|
2809
|
+
// this.onUpdated();
|
|
2781
2810
|
this.refresh(isUpdate);
|
|
2782
2811
|
}
|
|
2783
2812
|
/**
|
|
@@ -2791,8 +2820,8 @@
|
|
|
2791
2820
|
* @param {boolean} value - True to skip events; false to restore normal behavior.
|
|
2792
2821
|
*/
|
|
2793
2822
|
skipEvent(value) {
|
|
2794
|
-
if (this.
|
|
2795
|
-
this.
|
|
2823
|
+
if (this.privAdapterHandle)
|
|
2824
|
+
this.privAdapterHandle.isSkipEvent = value;
|
|
2796
2825
|
}
|
|
2797
2826
|
/**
|
|
2798
2827
|
* Re-renders the recycler view if present and invokes the post-refresh hook.
|
|
@@ -2801,9 +2830,9 @@
|
|
|
2801
2830
|
* @param isUpdate - Indicates if this refresh is due to an update operation.
|
|
2802
2831
|
*/
|
|
2803
2832
|
refresh(isUpdate) {
|
|
2804
|
-
if (!this.
|
|
2833
|
+
if (!this.privRecyclerViewHandle)
|
|
2805
2834
|
return;
|
|
2806
|
-
this.
|
|
2835
|
+
this.privRecyclerViewHandle.refresh(isUpdate);
|
|
2807
2836
|
this.onUpdated();
|
|
2808
2837
|
}
|
|
2809
2838
|
/**
|
|
@@ -2812,9 +2841,9 @@
|
|
|
2812
2841
|
*/
|
|
2813
2842
|
getResources() {
|
|
2814
2843
|
return {
|
|
2815
|
-
modelList: this.
|
|
2816
|
-
adapter: this.
|
|
2817
|
-
recyclerView: this.
|
|
2844
|
+
modelList: this.privModelList,
|
|
2845
|
+
adapter: this.privAdapterHandle,
|
|
2846
|
+
recyclerView: this.privRecyclerViewHandle,
|
|
2818
2847
|
};
|
|
2819
2848
|
}
|
|
2820
2849
|
/**
|
|
@@ -2822,14 +2851,14 @@
|
|
|
2822
2851
|
* enabling observers to react before a change is applied.
|
|
2823
2852
|
*/
|
|
2824
2853
|
triggerChanging(event_name) {
|
|
2825
|
-
this.
|
|
2854
|
+
return this.privAdapterHandle?.changingProp(event_name);
|
|
2826
2855
|
}
|
|
2827
2856
|
/**
|
|
2828
2857
|
* Triggers the adapter's post-change pipeline for a named event,
|
|
2829
2858
|
* notifying observers after a change has been applied.
|
|
2830
2859
|
*/
|
|
2831
2860
|
triggerChanged(event_name) {
|
|
2832
|
-
this.
|
|
2861
|
+
return this.privAdapterHandle?.changeProp(event_name);
|
|
2833
2862
|
}
|
|
2834
2863
|
}
|
|
2835
2864
|
|
|
@@ -2920,6 +2949,7 @@
|
|
|
2920
2949
|
this.selectUIMask = null;
|
|
2921
2950
|
this.parentMask = null;
|
|
2922
2951
|
this.modelManager = null;
|
|
2952
|
+
this.modelDatas = [];
|
|
2923
2953
|
if (options)
|
|
2924
2954
|
this.init(options);
|
|
2925
2955
|
}
|
|
@@ -2959,7 +2989,10 @@
|
|
|
2959
2989
|
* Keeps the accessory box aligned relative to the parent mask.
|
|
2960
2990
|
*/
|
|
2961
2991
|
refreshLocation() {
|
|
2962
|
-
if (!this.parentMask ||
|
|
2992
|
+
if (!this.parentMask ||
|
|
2993
|
+
!this.node ||
|
|
2994
|
+
!this.selectUIMask ||
|
|
2995
|
+
!this.options)
|
|
2963
2996
|
return;
|
|
2964
2997
|
const ref = this.options.accessoryStyle === "top"
|
|
2965
2998
|
? this.selectUIMask
|
|
@@ -2985,7 +3018,6 @@
|
|
|
2985
3018
|
return;
|
|
2986
3019
|
this.node.replaceChildren();
|
|
2987
3020
|
if (modelDatas.length > 0 && this.options.multiple) {
|
|
2988
|
-
this.node.classList.remove("hide");
|
|
2989
3021
|
modelDatas.forEach((modelData) => {
|
|
2990
3022
|
Libs.mountNode({
|
|
2991
3023
|
AccessoryItem: {
|
|
@@ -2998,12 +3030,10 @@
|
|
|
2998
3030
|
role: "button",
|
|
2999
3031
|
ariaLabel: `${this.options.textAccessoryDeselect}${modelData.textContent}`,
|
|
3000
3032
|
title: `${this.options.textAccessoryDeselect}${modelData.textContent}`,
|
|
3001
|
-
onclick: (evt) => {
|
|
3033
|
+
onclick: async (evt) => {
|
|
3002
3034
|
evt.preventDefault();
|
|
3003
|
-
this.modelManager?.triggerChanging?.("select");
|
|
3004
|
-
|
|
3005
|
-
modelData.selected = false;
|
|
3006
|
-
}, 10);
|
|
3035
|
+
await this.modelManager?.triggerChanging?.("select");
|
|
3036
|
+
modelData.selected = false;
|
|
3007
3037
|
},
|
|
3008
3038
|
},
|
|
3009
3039
|
},
|
|
@@ -3020,10 +3050,26 @@
|
|
|
3020
3050
|
});
|
|
3021
3051
|
}
|
|
3022
3052
|
else {
|
|
3023
|
-
|
|
3053
|
+
modelDatas = [];
|
|
3024
3054
|
}
|
|
3055
|
+
this.modelDatas = modelDatas;
|
|
3056
|
+
this.refreshDisplay();
|
|
3025
3057
|
iEvents.trigger(window, "resize");
|
|
3026
3058
|
}
|
|
3059
|
+
refreshDisplay() {
|
|
3060
|
+
if (this.options?.accessoryVisible && this.modelDatas.length > 0 && this.options.multiple) {
|
|
3061
|
+
this.show();
|
|
3062
|
+
}
|
|
3063
|
+
else {
|
|
3064
|
+
this.hide();
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
show() {
|
|
3068
|
+
this.node.classList.remove("hide");
|
|
3069
|
+
}
|
|
3070
|
+
hide() {
|
|
3071
|
+
this.node.classList.add("hide");
|
|
3072
|
+
}
|
|
3027
3073
|
}
|
|
3028
3074
|
|
|
3029
3075
|
class SearchController {
|
|
@@ -3036,11 +3082,11 @@
|
|
|
3036
3082
|
* @param {SelectBox} selectBox - SelectBox handle.
|
|
3037
3083
|
*/
|
|
3038
3084
|
constructor(selectElement, modelManager, selectBox) {
|
|
3039
|
-
this.
|
|
3040
|
-
this.
|
|
3041
|
-
this.
|
|
3042
|
-
this.
|
|
3043
|
-
this.
|
|
3085
|
+
this.ajaxConfig = null;
|
|
3086
|
+
this.abortController = null;
|
|
3087
|
+
this.popup = null;
|
|
3088
|
+
this.selectBox = null;
|
|
3089
|
+
this.paginationState = {
|
|
3044
3090
|
currentPage: 0,
|
|
3045
3091
|
totalPages: 1,
|
|
3046
3092
|
hasMore: false,
|
|
@@ -3048,9 +3094,9 @@
|
|
|
3048
3094
|
currentKeyword: "",
|
|
3049
3095
|
isPaginationEnabled: false,
|
|
3050
3096
|
};
|
|
3051
|
-
this.
|
|
3052
|
-
this.
|
|
3053
|
-
this.
|
|
3097
|
+
this.select = selectElement;
|
|
3098
|
+
this.modelManager = modelManager;
|
|
3099
|
+
this.selectBox = selectBox;
|
|
3054
3100
|
}
|
|
3055
3101
|
/**
|
|
3056
3102
|
* Indicates whether AJAX-based search is configured.
|
|
@@ -3058,7 +3104,7 @@
|
|
|
3058
3104
|
* @returns {boolean} - True if AJAX config is present; false otherwise.
|
|
3059
3105
|
*/
|
|
3060
3106
|
isAjax() {
|
|
3061
|
-
return !!this.
|
|
3107
|
+
return !!this.ajaxConfig;
|
|
3062
3108
|
}
|
|
3063
3109
|
/**
|
|
3064
3110
|
* Load specific options by their values from server
|
|
@@ -3066,14 +3112,14 @@
|
|
|
3066
3112
|
* @returns {Promise<{success: boolean, items: Array, message?: string}>}
|
|
3067
3113
|
*/
|
|
3068
3114
|
async loadByValues(values) {
|
|
3069
|
-
if (!this.
|
|
3115
|
+
if (!this.ajaxConfig) {
|
|
3070
3116
|
return { success: false, items: [], message: "Ajax not configured" };
|
|
3071
3117
|
}
|
|
3072
3118
|
const valuesArray = Array.isArray(values) ? values : [values];
|
|
3073
3119
|
if (valuesArray.length === 0)
|
|
3074
3120
|
return { success: true, items: [] };
|
|
3075
3121
|
try {
|
|
3076
|
-
const cfg = this.
|
|
3122
|
+
const cfg = this.ajaxConfig;
|
|
3077
3123
|
let payload;
|
|
3078
3124
|
if (typeof cfg.dataByValues === "function") {
|
|
3079
3125
|
payload = cfg.dataByValues(valuesArray);
|
|
@@ -3082,7 +3128,7 @@
|
|
|
3082
3128
|
payload = {
|
|
3083
3129
|
values: valuesArray.join(","),
|
|
3084
3130
|
load_by_values: "1",
|
|
3085
|
-
...(typeof cfg.data === "function" ? cfg.data.bind(this.
|
|
3131
|
+
...(typeof cfg.data === "function" ? cfg.data.bind(this.selectBox.Selective.find(this.selectBox.container.targetElement))("", 0) : cfg.data ?? {}),
|
|
3086
3132
|
};
|
|
3087
3133
|
}
|
|
3088
3134
|
let response;
|
|
@@ -3102,7 +3148,7 @@
|
|
|
3102
3148
|
if (!response.ok)
|
|
3103
3149
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
3104
3150
|
const data = await response.json();
|
|
3105
|
-
const result = this.
|
|
3151
|
+
const result = this.parseResponse(data);
|
|
3106
3152
|
return { success: true, items: result.items };
|
|
3107
3153
|
}
|
|
3108
3154
|
catch (error) {
|
|
@@ -3116,7 +3162,7 @@
|
|
|
3116
3162
|
* @returns {{existing: string[], missing: string[]}}
|
|
3117
3163
|
*/
|
|
3118
3164
|
checkMissingValues(values) {
|
|
3119
|
-
const allOptions = Array.from(this.
|
|
3165
|
+
const allOptions = Array.from(this.select.options);
|
|
3120
3166
|
const existingValues = allOptions.map((opt) => opt.value);
|
|
3121
3167
|
const existing = values.filter((v) => existingValues.includes(v));
|
|
3122
3168
|
const missing = values.filter((v) => !existingValues.includes(v));
|
|
@@ -3128,7 +3174,7 @@
|
|
|
3128
3174
|
* @param {object} config - AJAX configuration object (e.g., endpoint, headers, query params).
|
|
3129
3175
|
*/
|
|
3130
3176
|
setAjax(config) {
|
|
3131
|
-
this.
|
|
3177
|
+
this.ajaxConfig = config;
|
|
3132
3178
|
}
|
|
3133
3179
|
/**
|
|
3134
3180
|
* Attaches a Popup instance to allow UI updates during search (e.g., loading, resize).
|
|
@@ -3136,34 +3182,34 @@
|
|
|
3136
3182
|
* @param {Popup} popupInstance - The popup used to display search results and loading state.
|
|
3137
3183
|
*/
|
|
3138
3184
|
setPopup(popupInstance) {
|
|
3139
|
-
this.
|
|
3185
|
+
this.popup = popupInstance;
|
|
3140
3186
|
}
|
|
3141
3187
|
/**
|
|
3142
3188
|
* Returns a shallow copy of the current pagination state used for search/infinite scroll.
|
|
3143
3189
|
*/
|
|
3144
3190
|
getPaginationState() {
|
|
3145
|
-
return { ...this.
|
|
3191
|
+
return { ...this.paginationState };
|
|
3146
3192
|
}
|
|
3147
3193
|
/**
|
|
3148
3194
|
* Resets pagination counters while preserving whether pagination is enabled.
|
|
3149
3195
|
* Clears page, totals, loading flags, and current keyword.
|
|
3150
3196
|
*/
|
|
3151
3197
|
resetPagination() {
|
|
3152
|
-
this.
|
|
3198
|
+
this.paginationState = {
|
|
3153
3199
|
currentPage: 0,
|
|
3154
3200
|
totalPages: 1,
|
|
3155
3201
|
hasMore: false,
|
|
3156
3202
|
isLoading: false,
|
|
3157
3203
|
currentKeyword: "",
|
|
3158
|
-
isPaginationEnabled: this.
|
|
3204
|
+
isPaginationEnabled: this.paginationState.isPaginationEnabled,
|
|
3159
3205
|
};
|
|
3160
3206
|
}
|
|
3161
3207
|
/**
|
|
3162
3208
|
* Clears the current keyword and makes all options visible (local reset).
|
|
3163
3209
|
*/
|
|
3164
3210
|
clear() {
|
|
3165
|
-
this.
|
|
3166
|
-
const { modelList } = this.
|
|
3211
|
+
this.paginationState.currentKeyword = "";
|
|
3212
|
+
const { modelList } = this.modelManager.getResources();
|
|
3167
3213
|
const flatOptions = [];
|
|
3168
3214
|
for (const m of modelList) {
|
|
3169
3215
|
if (m instanceof OptionModel)
|
|
@@ -3179,7 +3225,7 @@
|
|
|
3179
3225
|
* Performs a search with either AJAX or local filtering depending on configuration.
|
|
3180
3226
|
*/
|
|
3181
3227
|
async search(keyword, append = false) {
|
|
3182
|
-
if (this.
|
|
3228
|
+
if (this.ajaxConfig)
|
|
3183
3229
|
return this._ajaxSearch(keyword, append);
|
|
3184
3230
|
return this._localSearch(keyword);
|
|
3185
3231
|
}
|
|
@@ -3187,16 +3233,16 @@
|
|
|
3187
3233
|
* Loads the next page for AJAX pagination if enabled and not already loading.
|
|
3188
3234
|
*/
|
|
3189
3235
|
async loadMore() {
|
|
3190
|
-
if (!this.
|
|
3236
|
+
if (!this.ajaxConfig)
|
|
3191
3237
|
return { success: false, message: "Ajax not enabled" };
|
|
3192
|
-
if (this.
|
|
3238
|
+
if (this.paginationState.isLoading)
|
|
3193
3239
|
return { success: false, message: "Already loading" };
|
|
3194
|
-
if (!this.
|
|
3240
|
+
if (!this.paginationState.isPaginationEnabled)
|
|
3195
3241
|
return { success: false, message: "Pagination not enabled" };
|
|
3196
|
-
if (!this.
|
|
3242
|
+
if (!this.paginationState.hasMore)
|
|
3197
3243
|
return { success: false, message: "No more data" };
|
|
3198
|
-
this.
|
|
3199
|
-
return this._ajaxSearch(this.
|
|
3244
|
+
this.paginationState.currentPage++;
|
|
3245
|
+
return this._ajaxSearch(this.paginationState.currentKeyword, true);
|
|
3200
3246
|
}
|
|
3201
3247
|
/**
|
|
3202
3248
|
* Executes a local (in-memory) search by normalizing the keyword (lowercase, non-accent)
|
|
@@ -3204,10 +3250,10 @@
|
|
|
3204
3250
|
*/
|
|
3205
3251
|
async _localSearch(keyword) {
|
|
3206
3252
|
if (this.compareSearchTrigger(keyword))
|
|
3207
|
-
this.
|
|
3253
|
+
this.paginationState.currentKeyword = keyword;
|
|
3208
3254
|
const lower = String(keyword ?? "").toLowerCase();
|
|
3209
3255
|
const lowerNA = Libs.string2normalize(lower);
|
|
3210
|
-
const { modelList } = this.
|
|
3256
|
+
const { modelList } = this.modelManager.getResources();
|
|
3211
3257
|
const flatOptions = [];
|
|
3212
3258
|
for (const m of modelList) {
|
|
3213
3259
|
if (m instanceof OptionModel)
|
|
@@ -3233,29 +3279,29 @@
|
|
|
3233
3279
|
* to determine if a new search should be triggered.
|
|
3234
3280
|
*/
|
|
3235
3281
|
compareSearchTrigger(keyword) {
|
|
3236
|
-
return keyword !== this.
|
|
3282
|
+
return keyword !== this.paginationState.currentKeyword;
|
|
3237
3283
|
}
|
|
3238
3284
|
/**
|
|
3239
3285
|
* Executes an AJAX-based search with optional appending.
|
|
3240
3286
|
*/
|
|
3241
3287
|
async _ajaxSearch(keyword, append = false) {
|
|
3242
|
-
const cfg = this.
|
|
3288
|
+
const cfg = this.ajaxConfig;
|
|
3243
3289
|
if (this.compareSearchTrigger(keyword)) {
|
|
3244
3290
|
this.resetPagination();
|
|
3245
|
-
this.
|
|
3291
|
+
this.paginationState.currentKeyword = keyword;
|
|
3246
3292
|
append = false;
|
|
3247
3293
|
}
|
|
3248
|
-
this.
|
|
3249
|
-
this.
|
|
3250
|
-
this.
|
|
3251
|
-
this.
|
|
3252
|
-
const page = this.
|
|
3253
|
-
const selectedValues = Array.from(this.
|
|
3294
|
+
this.paginationState.isLoading = true;
|
|
3295
|
+
this.popup?.showLoading();
|
|
3296
|
+
this.abortController?.abort();
|
|
3297
|
+
this.abortController = new AbortController();
|
|
3298
|
+
const page = this.paginationState.currentPage;
|
|
3299
|
+
const selectedValues = Array.from(this.select.selectedOptions)
|
|
3254
3300
|
.map((opt) => opt.value)
|
|
3255
3301
|
.join(",");
|
|
3256
3302
|
let payload;
|
|
3257
3303
|
if (typeof cfg.data === "function") {
|
|
3258
|
-
payload = cfg.data.bind(this.
|
|
3304
|
+
payload = cfg.data.bind(this.selectBox.Selective.find(this.selectBox.container.targetElement))(keyword, page);
|
|
3259
3305
|
if (payload && typeof payload.selectedValue === "undefined")
|
|
3260
3306
|
payload.selectedValue = selectedValues;
|
|
3261
3307
|
}
|
|
@@ -3271,27 +3317,27 @@
|
|
|
3271
3317
|
method: "POST",
|
|
3272
3318
|
body: formData,
|
|
3273
3319
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
3274
|
-
signal: this.
|
|
3320
|
+
signal: this.abortController.signal,
|
|
3275
3321
|
});
|
|
3276
3322
|
}
|
|
3277
3323
|
else {
|
|
3278
3324
|
const params = new URLSearchParams(payload).toString();
|
|
3279
|
-
response = await fetch(`${cfg.url}?${params}`, { signal: this.
|
|
3325
|
+
response = await fetch(`${cfg.url}?${params}`, { signal: this.abortController.signal });
|
|
3280
3326
|
}
|
|
3281
3327
|
const data = await response.json();
|
|
3282
|
-
const result = this.
|
|
3328
|
+
const result = this.parseResponse(data);
|
|
3283
3329
|
if (result.hasPagination) {
|
|
3284
|
-
this.
|
|
3285
|
-
this.
|
|
3286
|
-
this.
|
|
3287
|
-
this.
|
|
3330
|
+
this.paginationState.isPaginationEnabled = true;
|
|
3331
|
+
this.paginationState.currentPage = result.page;
|
|
3332
|
+
this.paginationState.totalPages = result.totalPages;
|
|
3333
|
+
this.paginationState.hasMore = result.hasMore;
|
|
3288
3334
|
}
|
|
3289
3335
|
else {
|
|
3290
|
-
this.
|
|
3336
|
+
this.paginationState.isPaginationEnabled = false;
|
|
3291
3337
|
}
|
|
3292
3338
|
this.applyAjaxResult(result.items, !!cfg.keepSelected, append);
|
|
3293
|
-
this.
|
|
3294
|
-
this.
|
|
3339
|
+
this.paginationState.isLoading = false;
|
|
3340
|
+
this.popup?.hideLoading();
|
|
3295
3341
|
return {
|
|
3296
3342
|
success: true,
|
|
3297
3343
|
hasResults: result.items.length > 0,
|
|
@@ -3303,8 +3349,8 @@
|
|
|
3303
3349
|
};
|
|
3304
3350
|
}
|
|
3305
3351
|
catch (error) {
|
|
3306
|
-
this.
|
|
3307
|
-
this.
|
|
3352
|
+
this.paginationState.isLoading = false;
|
|
3353
|
+
this.popup?.hideLoading();
|
|
3308
3354
|
if (error?.name === "AbortError")
|
|
3309
3355
|
return { success: false, message: "Request aborted" };
|
|
3310
3356
|
console.error("Ajax search error:", error);
|
|
@@ -3314,7 +3360,7 @@
|
|
|
3314
3360
|
/**
|
|
3315
3361
|
* Parses various server response shapes into a normalized structure for options and groups.
|
|
3316
3362
|
*/
|
|
3317
|
-
|
|
3363
|
+
parseResponse(data) {
|
|
3318
3364
|
let items = [];
|
|
3319
3365
|
let hasPagination = false;
|
|
3320
3366
|
let page = 0;
|
|
@@ -3381,7 +3427,7 @@
|
|
|
3381
3427
|
* Applies normalized AJAX results to the underlying <select> element.
|
|
3382
3428
|
*/
|
|
3383
3429
|
applyAjaxResult(items, keepSelected, append = false) {
|
|
3384
|
-
const select = this.
|
|
3430
|
+
const select = this.select;
|
|
3385
3431
|
let oldSelected = [];
|
|
3386
3432
|
if (keepSelected)
|
|
3387
3433
|
oldSelected = Array.from(select.selectedOptions).map((o) => o.value);
|
|
@@ -3436,7 +3482,6 @@
|
|
|
3436
3482
|
select.appendChild(option);
|
|
3437
3483
|
}
|
|
3438
3484
|
});
|
|
3439
|
-
select.dispatchEvent(new CustomEvent("options:changed"));
|
|
3440
3485
|
}
|
|
3441
3486
|
}
|
|
3442
3487
|
|
|
@@ -3446,29 +3491,22 @@
|
|
|
3446
3491
|
class SelectObserver {
|
|
3447
3492
|
/**
|
|
3448
3493
|
* Initializes the SelectObserver for a given <select> element.
|
|
3449
|
-
* Captures the initial snapshot, sets up a MutationObserver
|
|
3494
|
+
* Captures the initial snapshot, sets up a MutationObserver.
|
|
3450
3495
|
* Changes are debounced to prevent excessive calls.
|
|
3451
3496
|
*
|
|
3452
3497
|
* @param {HTMLSelectElement} select - The <select> element to observe.
|
|
3453
3498
|
*/
|
|
3454
3499
|
constructor(select) {
|
|
3455
|
-
this.
|
|
3456
|
-
this.
|
|
3500
|
+
this.debounceTimer = null;
|
|
3501
|
+
this.lastSnapshot = null;
|
|
3457
3502
|
this._DEBOUNCE_DELAY = 50;
|
|
3458
|
-
this.
|
|
3459
|
-
this.
|
|
3460
|
-
this.
|
|
3461
|
-
if (this.
|
|
3462
|
-
clearTimeout(this.
|
|
3463
|
-
this.
|
|
3464
|
-
this.
|
|
3465
|
-
}, this._DEBOUNCE_DELAY);
|
|
3466
|
-
});
|
|
3467
|
-
select.addEventListener("options:changed", () => {
|
|
3468
|
-
if (this._debounceTimer)
|
|
3469
|
-
clearTimeout(this._debounceTimer);
|
|
3470
|
-
this._debounceTimer = setTimeout(() => {
|
|
3471
|
-
this._handleChange();
|
|
3503
|
+
this.select = select;
|
|
3504
|
+
this.lastSnapshot = this.createSnapshot();
|
|
3505
|
+
this.observer = new MutationObserver(() => {
|
|
3506
|
+
if (this.debounceTimer)
|
|
3507
|
+
clearTimeout(this.debounceTimer);
|
|
3508
|
+
this.debounceTimer = setTimeout(() => {
|
|
3509
|
+
this.handleChange();
|
|
3472
3510
|
}, this._DEBOUNCE_DELAY);
|
|
3473
3511
|
});
|
|
3474
3512
|
}
|
|
@@ -3478,8 +3516,8 @@
|
|
|
3478
3516
|
*
|
|
3479
3517
|
* @returns {SelectSnapshot} A snapshot of the options state.
|
|
3480
3518
|
*/
|
|
3481
|
-
|
|
3482
|
-
const options = Array.from(this.
|
|
3519
|
+
createSnapshot() {
|
|
3520
|
+
const options = Array.from(this.select.options);
|
|
3483
3521
|
return {
|
|
3484
3522
|
length: options.length,
|
|
3485
3523
|
values: options.map((opt) => opt.value).join(","),
|
|
@@ -3493,28 +3531,28 @@
|
|
|
3493
3531
|
*
|
|
3494
3532
|
* @returns {boolean} True if a real change occurred, otherwise false.
|
|
3495
3533
|
*/
|
|
3496
|
-
|
|
3497
|
-
const newSnapshot = this.
|
|
3498
|
-
const changed = JSON.stringify(newSnapshot) !== JSON.stringify(this.
|
|
3534
|
+
hasRealChange() {
|
|
3535
|
+
const newSnapshot = this.createSnapshot();
|
|
3536
|
+
const changed = JSON.stringify(newSnapshot) !== JSON.stringify(this.lastSnapshot);
|
|
3499
3537
|
if (changed)
|
|
3500
|
-
this.
|
|
3538
|
+
this.lastSnapshot = newSnapshot;
|
|
3501
3539
|
return changed;
|
|
3502
3540
|
}
|
|
3503
3541
|
/**
|
|
3504
3542
|
* Handles detected changes after debouncing.
|
|
3505
3543
|
* If a real change is found, invokes the onChanged() hook with the current <select> element.
|
|
3506
3544
|
*/
|
|
3507
|
-
|
|
3508
|
-
if (!this.
|
|
3545
|
+
handleChange() {
|
|
3546
|
+
if (!this.hasRealChange())
|
|
3509
3547
|
return;
|
|
3510
|
-
this.onChanged(this.
|
|
3548
|
+
this.onChanged(this.select);
|
|
3511
3549
|
}
|
|
3512
3550
|
/**
|
|
3513
3551
|
* Starts observing the <select> element for child list mutations and attribute changes.
|
|
3514
3552
|
* Uses MutationObserver with a debounce mechanism to batch rapid updates.
|
|
3515
3553
|
*/
|
|
3516
3554
|
connect() {
|
|
3517
|
-
this.
|
|
3555
|
+
this.observer.observe(this.select, {
|
|
3518
3556
|
childList: true,
|
|
3519
3557
|
subtree: false,
|
|
3520
3558
|
attributes: true,
|
|
@@ -3536,10 +3574,10 @@
|
|
|
3536
3574
|
* Ensures no further change handling occurs after disconnecting.
|
|
3537
3575
|
*/
|
|
3538
3576
|
disconnect() {
|
|
3539
|
-
if (this.
|
|
3540
|
-
clearTimeout(this.
|
|
3541
|
-
this.
|
|
3542
|
-
this.
|
|
3577
|
+
if (this.debounceTimer)
|
|
3578
|
+
clearTimeout(this.debounceTimer);
|
|
3579
|
+
this.debounceTimer = null;
|
|
3580
|
+
this.observer.disconnect();
|
|
3543
3581
|
}
|
|
3544
3582
|
}
|
|
3545
3583
|
|
|
@@ -3554,9 +3592,9 @@
|
|
|
3554
3592
|
* @param {HTMLElement} element - The element whose dataset (data-* attributes) will be observed.
|
|
3555
3593
|
*/
|
|
3556
3594
|
constructor(element) {
|
|
3557
|
-
this.
|
|
3558
|
-
this.
|
|
3559
|
-
this.
|
|
3595
|
+
this.debounceTimer = null;
|
|
3596
|
+
this.element = element;
|
|
3597
|
+
this.observer = new MutationObserver((mutations) => {
|
|
3560
3598
|
let datasetChanged = false;
|
|
3561
3599
|
for (const mutation of mutations) {
|
|
3562
3600
|
if (mutation.type === "attributes" && mutation.attributeName?.startsWith("data-")) {
|
|
@@ -3566,14 +3604,14 @@
|
|
|
3566
3604
|
}
|
|
3567
3605
|
if (!datasetChanged)
|
|
3568
3606
|
return;
|
|
3569
|
-
if (this.
|
|
3570
|
-
clearTimeout(this.
|
|
3571
|
-
this.
|
|
3572
|
-
this.onChanged({ ...this.
|
|
3607
|
+
if (this.debounceTimer)
|
|
3608
|
+
clearTimeout(this.debounceTimer);
|
|
3609
|
+
this.debounceTimer = setTimeout(() => {
|
|
3610
|
+
this.onChanged({ ...this.element.dataset });
|
|
3573
3611
|
}, 50);
|
|
3574
3612
|
});
|
|
3575
3613
|
element.addEventListener("dataset:changed", () => {
|
|
3576
|
-
this.onChanged({ ...this.
|
|
3614
|
+
this.onChanged({ ...this.element.dataset });
|
|
3577
3615
|
});
|
|
3578
3616
|
}
|
|
3579
3617
|
/**
|
|
@@ -3581,7 +3619,7 @@
|
|
|
3581
3619
|
* Uses MutationObserver to track updates to data-* attributes.
|
|
3582
3620
|
*/
|
|
3583
3621
|
connect() {
|
|
3584
|
-
this.
|
|
3622
|
+
this.observer.observe(this.element, {
|
|
3585
3623
|
attributes: true,
|
|
3586
3624
|
attributeOldValue: true,
|
|
3587
3625
|
});
|
|
@@ -3600,10 +3638,10 @@
|
|
|
3600
3638
|
* Stops observing the element and clears any pending debounce timers.
|
|
3601
3639
|
*/
|
|
3602
3640
|
disconnect() {
|
|
3603
|
-
if (this.
|
|
3604
|
-
clearTimeout(this.
|
|
3605
|
-
this.
|
|
3606
|
-
this.
|
|
3641
|
+
if (this.debounceTimer)
|
|
3642
|
+
clearTimeout(this.debounceTimer);
|
|
3643
|
+
this.debounceTimer = null;
|
|
3644
|
+
this.observer.disconnect();
|
|
3607
3645
|
}
|
|
3608
3646
|
}
|
|
3609
3647
|
|
|
@@ -3656,7 +3694,7 @@
|
|
|
3656
3694
|
* @param {Function} callback - Function to execute before the property changes.
|
|
3657
3695
|
*/
|
|
3658
3696
|
onPropChanging(propName, callback) {
|
|
3659
|
-
Libs.callbackScheduler.on(`${propName}ing_${this.adapterKey}`, callback, { debounce:
|
|
3697
|
+
Libs.callbackScheduler.on(`${propName}ing_${this.adapterKey}`, callback, { debounce: 0 });
|
|
3660
3698
|
}
|
|
3661
3699
|
/**
|
|
3662
3700
|
* Registers a post-change callback for a property change pipeline.
|
|
@@ -3666,7 +3704,7 @@
|
|
|
3666
3704
|
* @param {Function} callback - Function to execute after the property changes.
|
|
3667
3705
|
*/
|
|
3668
3706
|
onPropChanged(propName, callback) {
|
|
3669
|
-
Libs.callbackScheduler.on(`${propName}_${this.adapterKey}`, callback);
|
|
3707
|
+
Libs.callbackScheduler.on(`${propName}_${this.adapterKey}`, callback, { debounce: 0 });
|
|
3670
3708
|
}
|
|
3671
3709
|
/**
|
|
3672
3710
|
* Triggers the post-change pipeline for a given property, passing optional parameters
|
|
@@ -3676,7 +3714,7 @@
|
|
|
3676
3714
|
* @param {...any} params - Parameters forwarded to the callbacks.
|
|
3677
3715
|
*/
|
|
3678
3716
|
changeProp(propName, ...params) {
|
|
3679
|
-
Libs.callbackScheduler.run(`${propName}_${this.adapterKey}`, ...params);
|
|
3717
|
+
return Libs.callbackScheduler.run(`${propName}_${this.adapterKey}`, ...params);
|
|
3680
3718
|
}
|
|
3681
3719
|
/**
|
|
3682
3720
|
* Triggers the pre-change pipeline for a given property, passing optional parameters
|
|
@@ -3686,7 +3724,7 @@
|
|
|
3686
3724
|
* @param {...any} params - Parameters forwarded to the callbacks.
|
|
3687
3725
|
*/
|
|
3688
3726
|
changingProp(propName, ...params) {
|
|
3689
|
-
Libs.callbackScheduler.run(`${propName}ing_${this.adapterKey}`, ...params);
|
|
3727
|
+
return Libs.callbackScheduler.run(`${propName}ing_${this.adapterKey}`, ...params);
|
|
3690
3728
|
}
|
|
3691
3729
|
/**
|
|
3692
3730
|
* Creates and returns a viewer instance for the given item within the specified parent container.
|
|
@@ -3713,10 +3751,10 @@
|
|
|
3713
3751
|
*
|
|
3714
3752
|
* @param {TItem[]} items - The new list of items to set.
|
|
3715
3753
|
*/
|
|
3716
|
-
setItems(items) {
|
|
3717
|
-
this.changingProp("items", items);
|
|
3754
|
+
async setItems(items) {
|
|
3755
|
+
await this.changingProp("items", items);
|
|
3718
3756
|
this.items = items;
|
|
3719
|
-
this.changeProp("items", items);
|
|
3757
|
+
await this.changeProp("items", items);
|
|
3720
3758
|
}
|
|
3721
3759
|
/**
|
|
3722
3760
|
* Synchronizes adapter items from an external source by delegating to setItems().
|
|
@@ -3724,8 +3762,8 @@
|
|
|
3724
3762
|
*
|
|
3725
3763
|
* @param {TItem[]} items - The source list of items to synchronize.
|
|
3726
3764
|
*/
|
|
3727
|
-
syncFromSource(items) {
|
|
3728
|
-
this.setItems(items);
|
|
3765
|
+
async syncFromSource(items) {
|
|
3766
|
+
await this.setItems(items);
|
|
3729
3767
|
}
|
|
3730
3768
|
/**
|
|
3731
3769
|
* Iterates through all items and ensures each has a viewer. For new items, calls viewHolder()
|
|
@@ -3903,10 +3941,10 @@
|
|
|
3903
3941
|
constructor(parent) {
|
|
3904
3942
|
super(parent);
|
|
3905
3943
|
this.view = null;
|
|
3906
|
-
this.
|
|
3907
|
-
this.
|
|
3908
|
-
this.
|
|
3909
|
-
this.
|
|
3944
|
+
this.config = null;
|
|
3945
|
+
this.configProxy = null;
|
|
3946
|
+
this.isRendered = false;
|
|
3947
|
+
this.setupConfigProxy();
|
|
3910
3948
|
}
|
|
3911
3949
|
/**
|
|
3912
3950
|
* Creates the internal configuration object and wraps it with a Proxy.
|
|
@@ -3914,9 +3952,9 @@
|
|
|
3914
3952
|
* applies only the necessary DOM changes for the updated property.
|
|
3915
3953
|
* No DOM mutations occur before the first render.
|
|
3916
3954
|
*/
|
|
3917
|
-
|
|
3955
|
+
setupConfigProxy() {
|
|
3918
3956
|
const self = this;
|
|
3919
|
-
this.
|
|
3957
|
+
this.config = {
|
|
3920
3958
|
isMultiple: false,
|
|
3921
3959
|
hasImage: false,
|
|
3922
3960
|
imagePosition: "right",
|
|
@@ -3926,7 +3964,7 @@
|
|
|
3926
3964
|
labelValign: "center",
|
|
3927
3965
|
labelHalign: "left",
|
|
3928
3966
|
};
|
|
3929
|
-
this.
|
|
3967
|
+
this.configProxy = new Proxy(this.config, {
|
|
3930
3968
|
set(target, prop, value) {
|
|
3931
3969
|
if (typeof prop !== "string")
|
|
3932
3970
|
return true;
|
|
@@ -3934,8 +3972,8 @@
|
|
|
3934
3972
|
const oldValue = target[key];
|
|
3935
3973
|
if (oldValue !== value) {
|
|
3936
3974
|
target[key] = value;
|
|
3937
|
-
if (self.
|
|
3938
|
-
self.
|
|
3975
|
+
if (self.isRendered) {
|
|
3976
|
+
self.applyPartialChange(key, value, oldValue);
|
|
3939
3977
|
}
|
|
3940
3978
|
}
|
|
3941
3979
|
return true;
|
|
@@ -3948,7 +3986,7 @@
|
|
|
3948
3986
|
* @returns {boolean} True if multiple selection is enabled; otherwise false.
|
|
3949
3987
|
*/
|
|
3950
3988
|
get isMultiple() {
|
|
3951
|
-
return this.
|
|
3989
|
+
return this.config.isMultiple;
|
|
3952
3990
|
}
|
|
3953
3991
|
/**
|
|
3954
3992
|
* Enables or disables multiple selection mode.
|
|
@@ -3957,7 +3995,7 @@
|
|
|
3957
3995
|
* @param {boolean} value - True to enable multiple selection; false for single selection.
|
|
3958
3996
|
*/
|
|
3959
3997
|
set isMultiple(value) {
|
|
3960
|
-
this.
|
|
3998
|
+
this.configProxy.isMultiple = !!value;
|
|
3961
3999
|
}
|
|
3962
4000
|
/**
|
|
3963
4001
|
* Indicates whether the option includes an image block alongside the label.
|
|
@@ -3965,7 +4003,7 @@
|
|
|
3965
4003
|
* @returns {boolean} True if an image is displayed; otherwise false.
|
|
3966
4004
|
*/
|
|
3967
4005
|
get hasImage() {
|
|
3968
|
-
return this.
|
|
4006
|
+
return this.config.hasImage;
|
|
3969
4007
|
}
|
|
3970
4008
|
/**
|
|
3971
4009
|
* Shows or hides the image block for the option.
|
|
@@ -3974,7 +4012,7 @@
|
|
|
3974
4012
|
* @param {boolean} value - True to show the image; false to hide it.
|
|
3975
4013
|
*/
|
|
3976
4014
|
set hasImage(value) {
|
|
3977
|
-
this.
|
|
4015
|
+
this.configProxy.hasImage = !!value;
|
|
3978
4016
|
}
|
|
3979
4017
|
/**
|
|
3980
4018
|
* Provides reactive access to the entire option configuration via a Proxy.
|
|
@@ -3983,7 +4021,7 @@
|
|
|
3983
4021
|
* @returns {object} The proxied configuration object.
|
|
3984
4022
|
*/
|
|
3985
4023
|
get optionConfig() {
|
|
3986
|
-
return this.
|
|
4024
|
+
return this.configProxy;
|
|
3987
4025
|
}
|
|
3988
4026
|
/**
|
|
3989
4027
|
* Applies a set of configuration changes in batch.
|
|
@@ -3991,23 +4029,23 @@
|
|
|
3991
4029
|
* When rendered, each changed property triggers a targeted DOM update via the proxy.
|
|
3992
4030
|
*/
|
|
3993
4031
|
set optionConfig(config) {
|
|
3994
|
-
if (!config || !this.
|
|
4032
|
+
if (!config || !this.configProxy || !this.config)
|
|
3995
4033
|
return;
|
|
3996
4034
|
const changes = {};
|
|
3997
|
-
if (config.imageWidth !== undefined && config.imageWidth !== this.
|
|
4035
|
+
if (config.imageWidth !== undefined && config.imageWidth !== this.config.imageWidth)
|
|
3998
4036
|
changes.imageWidth = config.imageWidth;
|
|
3999
|
-
if (config.imageHeight !== undefined && config.imageHeight !== this.
|
|
4037
|
+
if (config.imageHeight !== undefined && config.imageHeight !== this.config.imageHeight)
|
|
4000
4038
|
changes.imageHeight = config.imageHeight;
|
|
4001
|
-
if (config.imageBorderRadius !== undefined && config.imageBorderRadius !== this.
|
|
4039
|
+
if (config.imageBorderRadius !== undefined && config.imageBorderRadius !== this.config.imageBorderRadius)
|
|
4002
4040
|
changes.imageBorderRadius = config.imageBorderRadius;
|
|
4003
|
-
if (config.imagePosition !== undefined && config.imagePosition !== this.
|
|
4041
|
+
if (config.imagePosition !== undefined && config.imagePosition !== this.config.imagePosition)
|
|
4004
4042
|
changes.imagePosition = config.imagePosition;
|
|
4005
|
-
if (config.labelValign !== undefined && config.labelValign !== this.
|
|
4043
|
+
if (config.labelValign !== undefined && config.labelValign !== this.config.labelValign)
|
|
4006
4044
|
changes.labelValign = config.labelValign;
|
|
4007
|
-
if (config.labelHalign !== undefined && config.labelHalign !== this.
|
|
4045
|
+
if (config.labelHalign !== undefined && config.labelHalign !== this.config.labelHalign)
|
|
4008
4046
|
changes.labelHalign = config.labelHalign;
|
|
4009
4047
|
if (Object.keys(changes).length > 0)
|
|
4010
|
-
Object.assign(this.
|
|
4048
|
+
Object.assign(this.configProxy, changes);
|
|
4011
4049
|
}
|
|
4012
4050
|
/**
|
|
4013
4051
|
* Renders the option view into the parent element.
|
|
@@ -4019,30 +4057,30 @@
|
|
|
4019
4057
|
const viewClass = ["selective-ui-option-view"];
|
|
4020
4058
|
const opt_id = Libs.randomString(7);
|
|
4021
4059
|
const inputID = `option_${opt_id}`;
|
|
4022
|
-
if (this.
|
|
4060
|
+
if (this.config.isMultiple)
|
|
4023
4061
|
viewClass.push("multiple");
|
|
4024
|
-
if (this.
|
|
4062
|
+
if (this.config.hasImage) {
|
|
4025
4063
|
viewClass.push("has-image");
|
|
4026
|
-
viewClass.push(`image-${this.
|
|
4064
|
+
viewClass.push(`image-${this.config.imagePosition}`);
|
|
4027
4065
|
}
|
|
4028
4066
|
const childStructure = {
|
|
4029
4067
|
OptionInput: {
|
|
4030
4068
|
tag: {
|
|
4031
4069
|
node: "input",
|
|
4032
|
-
type: this.
|
|
4070
|
+
type: this.config.isMultiple ? "checkbox" : "radio",
|
|
4033
4071
|
classList: "allow-choice",
|
|
4034
4072
|
id: inputID,
|
|
4035
4073
|
},
|
|
4036
4074
|
},
|
|
4037
|
-
...(this.
|
|
4075
|
+
...(this.config.hasImage && {
|
|
4038
4076
|
OptionImage: {
|
|
4039
4077
|
tag: {
|
|
4040
4078
|
node: "img",
|
|
4041
4079
|
classList: "option-image",
|
|
4042
4080
|
style: {
|
|
4043
|
-
width: this.
|
|
4044
|
-
height: this.
|
|
4045
|
-
borderRadius: this.
|
|
4081
|
+
width: this.config.imageWidth,
|
|
4082
|
+
height: this.config.imageHeight,
|
|
4083
|
+
borderRadius: this.config.imageBorderRadius,
|
|
4046
4084
|
},
|
|
4047
4085
|
},
|
|
4048
4086
|
},
|
|
@@ -4052,8 +4090,8 @@
|
|
|
4052
4090
|
node: "label",
|
|
4053
4091
|
htmlFor: inputID,
|
|
4054
4092
|
classList: [
|
|
4055
|
-
`align-vertical-${this.
|
|
4056
|
-
`align-horizontal-${this.
|
|
4093
|
+
`align-vertical-${this.config.labelValign}`,
|
|
4094
|
+
`align-horizontal-${this.config.labelHalign}`,
|
|
4057
4095
|
],
|
|
4058
4096
|
},
|
|
4059
4097
|
child: {
|
|
@@ -4075,13 +4113,13 @@
|
|
|
4075
4113
|
},
|
|
4076
4114
|
});
|
|
4077
4115
|
this.parent.appendChild(this.view.view);
|
|
4078
|
-
this.
|
|
4116
|
+
this.isRendered = true;
|
|
4079
4117
|
}
|
|
4080
4118
|
/**
|
|
4081
4119
|
* Applies a targeted DOM update for a single configuration property change.
|
|
4082
4120
|
* Safely updates classes, attributes, styles, and child elements without re-rendering the whole view.
|
|
4083
4121
|
*/
|
|
4084
|
-
|
|
4122
|
+
applyPartialChange(prop, newValue, oldValue) {
|
|
4085
4123
|
const v = this.view;
|
|
4086
4124
|
if (!v || !v.view)
|
|
4087
4125
|
return;
|
|
@@ -4101,8 +4139,8 @@
|
|
|
4101
4139
|
const val = !!newValue;
|
|
4102
4140
|
root.classList.toggle("has-image", val);
|
|
4103
4141
|
if (val) {
|
|
4104
|
-
root.classList.add(`image-${this.
|
|
4105
|
-
this.
|
|
4142
|
+
root.classList.add(`image-${this.config.imagePosition}`);
|
|
4143
|
+
this.createImage();
|
|
4106
4144
|
}
|
|
4107
4145
|
else {
|
|
4108
4146
|
root.className = root.className.replace(/image-(top|right|bottom|left)/g, "").trim();
|
|
@@ -4115,7 +4153,7 @@
|
|
|
4115
4153
|
break;
|
|
4116
4154
|
}
|
|
4117
4155
|
case "imagePosition": {
|
|
4118
|
-
if (this.
|
|
4156
|
+
if (this.config.hasImage) {
|
|
4119
4157
|
root.className = root.className.replace(/image-(top|right|bottom|left)/g, "").trim();
|
|
4120
4158
|
root.classList.add(`image-${String(newValue)}`);
|
|
4121
4159
|
}
|
|
@@ -4138,7 +4176,7 @@
|
|
|
4138
4176
|
case "labelValign":
|
|
4139
4177
|
case "labelHalign": {
|
|
4140
4178
|
if (label) {
|
|
4141
|
-
const newClass = `align-vertical-${this.
|
|
4179
|
+
const newClass = `align-vertical-${this.config.labelValign} align-horizontal-${this.config.labelHalign}`;
|
|
4142
4180
|
if (label.className !== newClass)
|
|
4143
4181
|
label.className = newClass;
|
|
4144
4182
|
}
|
|
@@ -4152,7 +4190,7 @@
|
|
|
4152
4190
|
* The image receives configured styles (width, height, borderRadius) and is placed
|
|
4153
4191
|
* before the label if present; otherwise appended to the root. Updates `v.tags.OptionImage`.
|
|
4154
4192
|
*/
|
|
4155
|
-
|
|
4193
|
+
createImage() {
|
|
4156
4194
|
const v = this.view;
|
|
4157
4195
|
if (!v || !v.view)
|
|
4158
4196
|
return;
|
|
@@ -4163,9 +4201,9 @@
|
|
|
4163
4201
|
const label = v.tags?.OptionLabel;
|
|
4164
4202
|
const image = document.createElement("img");
|
|
4165
4203
|
image.className = "option-image";
|
|
4166
|
-
image.style.width = this.
|
|
4167
|
-
image.style.height = this.
|
|
4168
|
-
image.style.borderRadius = this.
|
|
4204
|
+
image.style.width = this.config.imageWidth;
|
|
4205
|
+
image.style.height = this.config.imageHeight;
|
|
4206
|
+
image.style.borderRadius = this.config.imageBorderRadius;
|
|
4169
4207
|
if (label && label.parentElement)
|
|
4170
4208
|
root.insertBefore(image, label);
|
|
4171
4209
|
else
|
|
@@ -4181,16 +4219,16 @@
|
|
|
4181
4219
|
constructor(items = []) {
|
|
4182
4220
|
super(items);
|
|
4183
4221
|
this.isMultiple = false;
|
|
4184
|
-
this.
|
|
4185
|
-
this.
|
|
4186
|
-
this.
|
|
4222
|
+
this.visibilityChangedCallbacks = [];
|
|
4223
|
+
this.currentHighlightIndex = -1;
|
|
4224
|
+
this.selectedItemSingle = null;
|
|
4187
4225
|
this.groups = [];
|
|
4188
4226
|
this.flatOptions = [];
|
|
4189
|
-
this.
|
|
4227
|
+
this.buildFlatStructure();
|
|
4190
4228
|
Libs.callbackScheduler.on(`sche_vis_${this.adapterKey}`, () => {
|
|
4191
4229
|
const visibleCount = this.flatOptions.filter((item) => item.visible).length;
|
|
4192
4230
|
const totalCount = this.flatOptions.length;
|
|
4193
|
-
this.
|
|
4231
|
+
this.visibilityChangedCallbacks.forEach((callback) => {
|
|
4194
4232
|
callback({
|
|
4195
4233
|
visibleCount,
|
|
4196
4234
|
totalCount,
|
|
@@ -4204,7 +4242,7 @@
|
|
|
4204
4242
|
/**
|
|
4205
4243
|
* Build flat list of all options for navigation
|
|
4206
4244
|
*/
|
|
4207
|
-
|
|
4245
|
+
buildFlatStructure() {
|
|
4208
4246
|
this.flatOptions = [];
|
|
4209
4247
|
this.groups = [];
|
|
4210
4248
|
this.items.forEach((item) => {
|
|
@@ -4240,10 +4278,10 @@
|
|
|
4240
4278
|
onViewHolder(item, viewer, position) {
|
|
4241
4279
|
item.position = position;
|
|
4242
4280
|
if (item instanceof GroupModel) {
|
|
4243
|
-
this.
|
|
4281
|
+
this.handleGroupView(item, viewer, position);
|
|
4244
4282
|
}
|
|
4245
4283
|
else if (item instanceof OptionModel) {
|
|
4246
|
-
this.
|
|
4284
|
+
this.handleOptionView(item, viewer, position);
|
|
4247
4285
|
}
|
|
4248
4286
|
item.isInit = true;
|
|
4249
4287
|
}
|
|
@@ -4255,7 +4293,7 @@
|
|
|
4255
4293
|
* @param {GroupView} groupView - The view instance that renders the group in the UI.
|
|
4256
4294
|
* @param {number} position - The position (index) of the group within a list.
|
|
4257
4295
|
*/
|
|
4258
|
-
|
|
4296
|
+
handleGroupView(groupModel, groupView, position) {
|
|
4259
4297
|
super.onViewHolder(groupModel, groupView, position);
|
|
4260
4298
|
groupModel.view = groupView;
|
|
4261
4299
|
const header = groupView.view.tags.GroupHeader;
|
|
@@ -4280,7 +4318,7 @@
|
|
|
4280
4318
|
if (!optionModel.isInit || !optionViewer) {
|
|
4281
4319
|
optionViewer = new OptionView(itemsContainer);
|
|
4282
4320
|
}
|
|
4283
|
-
this.
|
|
4321
|
+
this.handleOptionView(optionModel, optionViewer, idx);
|
|
4284
4322
|
optionModel.isInit = true;
|
|
4285
4323
|
});
|
|
4286
4324
|
groupView.setCollapsed(groupModel.collapsed);
|
|
@@ -4294,7 +4332,7 @@
|
|
|
4294
4332
|
* @param {OptionView} optionViewer - The view instance that renders the option in the UI.
|
|
4295
4333
|
* @param {number} position - The index of this option within its group's item list.
|
|
4296
4334
|
*/
|
|
4297
|
-
|
|
4335
|
+
handleOptionView(optionModel, optionViewer, position) {
|
|
4298
4336
|
optionViewer.isMultiple = this.isMultiple;
|
|
4299
4337
|
optionViewer.hasImage = optionModel.hasImage;
|
|
4300
4338
|
optionViewer.optionConfig = {
|
|
@@ -4320,24 +4358,20 @@
|
|
|
4320
4358
|
}
|
|
4321
4359
|
optionViewer.view.tags.LabelContent.innerHTML = optionModel.text;
|
|
4322
4360
|
if (!optionModel.isInit) {
|
|
4323
|
-
optionViewer.view.tags.OptionView.addEventListener("click", (ev) => {
|
|
4361
|
+
optionViewer.view.tags.OptionView.addEventListener("click", async (ev) => {
|
|
4324
4362
|
ev.stopPropagation();
|
|
4325
4363
|
ev.preventDefault();
|
|
4326
4364
|
if (this.isSkipEvent)
|
|
4327
4365
|
return;
|
|
4328
4366
|
if (this.isMultiple) {
|
|
4329
|
-
this.changingProp("select");
|
|
4330
|
-
|
|
4331
|
-
optionModel.selected = !optionModel.selected;
|
|
4332
|
-
}, 5);
|
|
4367
|
+
await this.changingProp("select");
|
|
4368
|
+
optionModel.selected = !optionModel.selected;
|
|
4333
4369
|
}
|
|
4334
4370
|
else if (optionModel.selected !== true) {
|
|
4335
|
-
this.changingProp("select");
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
optionModel.selected = true;
|
|
4340
|
-
}, 5);
|
|
4371
|
+
await this.changingProp("select");
|
|
4372
|
+
if (this.selectedItemSingle)
|
|
4373
|
+
this.selectedItemSingle.selected = false;
|
|
4374
|
+
optionModel.selected = true;
|
|
4341
4375
|
}
|
|
4342
4376
|
});
|
|
4343
4377
|
optionViewer.view.tags.OptionView.title = optionModel.textContent;
|
|
@@ -4351,16 +4385,16 @@
|
|
|
4351
4385
|
});
|
|
4352
4386
|
optionModel.onInternalSelected((_evtToken, _el, selected) => {
|
|
4353
4387
|
if (selected)
|
|
4354
|
-
this.
|
|
4388
|
+
this.selectedItemSingle = optionModel;
|
|
4355
4389
|
this.changeProp("selected_internal");
|
|
4356
4390
|
});
|
|
4357
4391
|
optionModel.onVisibilityChanged((_evtToken, model, _visible) => {
|
|
4358
4392
|
model.group?.updateVisibility();
|
|
4359
|
-
this.
|
|
4393
|
+
this.notifyVisibilityChanged();
|
|
4360
4394
|
});
|
|
4361
4395
|
}
|
|
4362
4396
|
if (optionModel.selected) {
|
|
4363
|
-
this.
|
|
4397
|
+
this.selectedItemSingle = optionModel;
|
|
4364
4398
|
optionModel.selectedNonTrigger = true;
|
|
4365
4399
|
}
|
|
4366
4400
|
}
|
|
@@ -4369,19 +4403,19 @@
|
|
|
4369
4403
|
*
|
|
4370
4404
|
* @param {Array<GroupModel|OptionModel>} items - The new collection of items to be displayed.
|
|
4371
4405
|
*/
|
|
4372
|
-
setItems(items) {
|
|
4373
|
-
this.changingProp("items", items);
|
|
4406
|
+
async setItems(items) {
|
|
4407
|
+
await this.changingProp("items", items);
|
|
4374
4408
|
this.items = items;
|
|
4375
|
-
this.
|
|
4376
|
-
this.changeProp("items", items);
|
|
4409
|
+
this.buildFlatStructure();
|
|
4410
|
+
await this.changeProp("items", items);
|
|
4377
4411
|
}
|
|
4378
4412
|
/**
|
|
4379
4413
|
* Synchronizes the component's items from an external source by delegating to setItems().
|
|
4380
4414
|
*
|
|
4381
4415
|
* @param {Array<GroupModel|OptionModel>} items - The new collection of items to sync.
|
|
4382
4416
|
*/
|
|
4383
|
-
syncFromSource(items) {
|
|
4384
|
-
this.setItems(items);
|
|
4417
|
+
async syncFromSource(items) {
|
|
4418
|
+
await this.setItems(items);
|
|
4385
4419
|
}
|
|
4386
4420
|
/**
|
|
4387
4421
|
* Updates the component's data items and rebuilds the internal flat structure
|
|
@@ -4391,7 +4425,7 @@
|
|
|
4391
4425
|
*/
|
|
4392
4426
|
updateData(items) {
|
|
4393
4427
|
this.items = items;
|
|
4394
|
-
this.
|
|
4428
|
+
this.buildFlatStructure();
|
|
4395
4429
|
}
|
|
4396
4430
|
/**
|
|
4397
4431
|
* Returns all option items that are currently selected.
|
|
@@ -4428,13 +4462,13 @@
|
|
|
4428
4462
|
* - Function to invoke when visibility stats change.
|
|
4429
4463
|
*/
|
|
4430
4464
|
onVisibilityChanged(callback) {
|
|
4431
|
-
this.
|
|
4465
|
+
this.visibilityChangedCallbacks.push(callback);
|
|
4432
4466
|
}
|
|
4433
4467
|
/**
|
|
4434
4468
|
* Notifies all registered visibility-change callbacks with up-to-date statistics.
|
|
4435
4469
|
* Computes visible and total counts, then emits aggregated state.
|
|
4436
4470
|
*/
|
|
4437
|
-
|
|
4471
|
+
notifyVisibilityChanged() {
|
|
4438
4472
|
Libs.callbackScheduler.run(`sche_vis_${this.adapterKey}`);
|
|
4439
4473
|
}
|
|
4440
4474
|
/**
|
|
@@ -4466,7 +4500,7 @@
|
|
|
4466
4500
|
const visibleOptions = this.flatOptions.filter((opt) => opt.visible);
|
|
4467
4501
|
if (visibleOptions.length === 0)
|
|
4468
4502
|
return;
|
|
4469
|
-
let currentVisibleIndex = visibleOptions.findIndex((opt) => opt === this.flatOptions[this.
|
|
4503
|
+
let currentVisibleIndex = visibleOptions.findIndex((opt) => opt === this.flatOptions[this.currentHighlightIndex]);
|
|
4470
4504
|
if (currentVisibleIndex === -1)
|
|
4471
4505
|
currentVisibleIndex = -1;
|
|
4472
4506
|
let nextVisibleIndex = currentVisibleIndex + direction;
|
|
@@ -4483,8 +4517,8 @@
|
|
|
4483
4517
|
* No-op if nothing is highlighted or the highlighted item is not visible.
|
|
4484
4518
|
*/
|
|
4485
4519
|
selectHighlighted() {
|
|
4486
|
-
if (this.
|
|
4487
|
-
const item = this.flatOptions[this.
|
|
4520
|
+
if (this.currentHighlightIndex > -1 && this.flatOptions[this.currentHighlightIndex]) {
|
|
4521
|
+
const item = this.flatOptions[this.currentHighlightIndex];
|
|
4488
4522
|
if (item.visible) {
|
|
4489
4523
|
const viewEl = item.view?.getView?.();
|
|
4490
4524
|
if (viewEl)
|
|
@@ -4511,15 +4545,15 @@
|
|
|
4511
4545
|
else {
|
|
4512
4546
|
index = 0;
|
|
4513
4547
|
}
|
|
4514
|
-
if (this.
|
|
4515
|
-
this.flatOptions[this.
|
|
4548
|
+
if (this.currentHighlightIndex > -1 && this.flatOptions[this.currentHighlightIndex]) {
|
|
4549
|
+
this.flatOptions[this.currentHighlightIndex].highlighted = false;
|
|
4516
4550
|
}
|
|
4517
4551
|
for (let i = index; i < this.flatOptions.length; i++) {
|
|
4518
4552
|
const item = this.flatOptions[i];
|
|
4519
4553
|
if (!item?.visible)
|
|
4520
4554
|
continue;
|
|
4521
4555
|
item.highlighted = true;
|
|
4522
|
-
this.
|
|
4556
|
+
this.currentHighlightIndex = i;
|
|
4523
4557
|
if (isScrollToView) {
|
|
4524
4558
|
const el = item.view?.getView?.();
|
|
4525
4559
|
if (el) {
|
|
@@ -4628,15 +4662,15 @@
|
|
|
4628
4662
|
this.firstMeasured = false;
|
|
4629
4663
|
this.start = 0;
|
|
4630
4664
|
this.end = -1;
|
|
4631
|
-
this.
|
|
4632
|
-
this.
|
|
4633
|
-
this.
|
|
4634
|
-
this.
|
|
4635
|
-
this.
|
|
4636
|
-
this.
|
|
4637
|
-
this.
|
|
4638
|
-
this.
|
|
4639
|
-
this.
|
|
4665
|
+
this.rafId = null;
|
|
4666
|
+
this.measureRaf = null;
|
|
4667
|
+
this.updating = false;
|
|
4668
|
+
this.suppressResize = false;
|
|
4669
|
+
this.lastRenderCount = 0;
|
|
4670
|
+
this.suspended = false;
|
|
4671
|
+
this.resumeResizeAfter = false;
|
|
4672
|
+
this.stickyCacheTick = 0;
|
|
4673
|
+
this.stickyCacheVal = 0;
|
|
4640
4674
|
this.measuredSum = 0;
|
|
4641
4675
|
this.measuredCount = 0;
|
|
4642
4676
|
}
|
|
@@ -4669,8 +4703,8 @@
|
|
|
4669
4703
|
?? this.viewElement.parentElement;
|
|
4670
4704
|
if (!this.scrollEl)
|
|
4671
4705
|
throw new Error("VirtualRecyclerView: scrollEl not found");
|
|
4672
|
-
this.
|
|
4673
|
-
this.scrollEl.addEventListener("scroll", this.
|
|
4706
|
+
this.boundOnScroll = this.onScroll.bind(this);
|
|
4707
|
+
this.scrollEl.addEventListener("scroll", this.boundOnScroll, { passive: true });
|
|
4674
4708
|
this.refresh(false);
|
|
4675
4709
|
this.attachResizeObserverOnce();
|
|
4676
4710
|
adapter?.onVisibilityChanged?.(() => this.refreshItem());
|
|
@@ -4680,14 +4714,14 @@
|
|
|
4680
4714
|
* Cancels pending frames and disconnects observers.
|
|
4681
4715
|
*/
|
|
4682
4716
|
suspend() {
|
|
4683
|
-
this.
|
|
4717
|
+
this.suspended = true;
|
|
4684
4718
|
this.cancelFrames();
|
|
4685
|
-
if (this.scrollEl && this.
|
|
4686
|
-
this.scrollEl.removeEventListener("scroll", this.
|
|
4719
|
+
if (this.scrollEl && this.boundOnScroll) {
|
|
4720
|
+
this.scrollEl.removeEventListener("scroll", this.boundOnScroll);
|
|
4687
4721
|
}
|
|
4688
4722
|
if (this.resizeObs) {
|
|
4689
4723
|
this.resizeObs.disconnect();
|
|
4690
|
-
this.
|
|
4724
|
+
this.resumeResizeAfter = true;
|
|
4691
4725
|
}
|
|
4692
4726
|
}
|
|
4693
4727
|
/**
|
|
@@ -4695,13 +4729,13 @@
|
|
|
4695
4729
|
* Re-attaches listeners and schedules window update.
|
|
4696
4730
|
*/
|
|
4697
4731
|
resume() {
|
|
4698
|
-
this.
|
|
4699
|
-
if (this.scrollEl && this.
|
|
4700
|
-
this.scrollEl.addEventListener("scroll", this.
|
|
4732
|
+
this.suspended = false;
|
|
4733
|
+
if (this.scrollEl && this.boundOnScroll) {
|
|
4734
|
+
this.scrollEl.addEventListener("scroll", this.boundOnScroll, { passive: true });
|
|
4701
4735
|
}
|
|
4702
|
-
if (this.
|
|
4736
|
+
if (this.resumeResizeAfter) {
|
|
4703
4737
|
this.attachResizeObserverOnce();
|
|
4704
|
-
this.
|
|
4738
|
+
this.resumeResizeAfter = false;
|
|
4705
4739
|
}
|
|
4706
4740
|
this.scheduleUpdateWindow();
|
|
4707
4741
|
}
|
|
@@ -4717,7 +4751,7 @@
|
|
|
4717
4751
|
if (!isUpdate)
|
|
4718
4752
|
this.refreshItem();
|
|
4719
4753
|
const count = this.adapter.itemCount();
|
|
4720
|
-
this.
|
|
4754
|
+
this.lastRenderCount = count;
|
|
4721
4755
|
if (count === 0) {
|
|
4722
4756
|
this.resetState();
|
|
4723
4757
|
return;
|
|
@@ -4759,8 +4793,8 @@
|
|
|
4759
4793
|
*/
|
|
4760
4794
|
dispose() {
|
|
4761
4795
|
this.cancelFrames();
|
|
4762
|
-
if (this.scrollEl && this.
|
|
4763
|
-
this.scrollEl.removeEventListener("scroll", this.
|
|
4796
|
+
if (this.scrollEl && this.boundOnScroll) {
|
|
4797
|
+
this.scrollEl.removeEventListener("scroll", this.boundOnScroll);
|
|
4764
4798
|
}
|
|
4765
4799
|
this.resizeObs?.disconnect();
|
|
4766
4800
|
this.created.forEach(el => el.remove());
|
|
@@ -4788,13 +4822,13 @@
|
|
|
4788
4822
|
}
|
|
4789
4823
|
/** Cancels all pending animation frames. */
|
|
4790
4824
|
cancelFrames() {
|
|
4791
|
-
if (this.
|
|
4792
|
-
cancelAnimationFrame(this.
|
|
4793
|
-
this.
|
|
4825
|
+
if (this.rafId != null) {
|
|
4826
|
+
cancelAnimationFrame(this.rafId);
|
|
4827
|
+
this.rafId = null;
|
|
4794
4828
|
}
|
|
4795
|
-
if (this.
|
|
4796
|
-
cancelAnimationFrame(this.
|
|
4797
|
-
this.
|
|
4829
|
+
if (this.measureRaf != null) {
|
|
4830
|
+
cancelAnimationFrame(this.measureRaf);
|
|
4831
|
+
this.measureRaf = null;
|
|
4798
4832
|
}
|
|
4799
4833
|
}
|
|
4800
4834
|
/** Resets all internal state: DOM, caches, measurements. */
|
|
@@ -4872,7 +4906,7 @@
|
|
|
4872
4906
|
containerTopInScroll() {
|
|
4873
4907
|
const a = this.viewElement.getBoundingClientRect();
|
|
4874
4908
|
const b = this.scrollEl.getBoundingClientRect();
|
|
4875
|
-
return a.top - b.top + this.scrollEl.scrollTop;
|
|
4909
|
+
return Math.max(0, a.top - b.top + this.scrollEl.scrollTop);
|
|
4876
4910
|
}
|
|
4877
4911
|
/**
|
|
4878
4912
|
* Returns sticky header height with 16ms cache to avoid DOM thrashing.
|
|
@@ -4880,19 +4914,19 @@
|
|
|
4880
4914
|
*/
|
|
4881
4915
|
stickyTopHeight() {
|
|
4882
4916
|
const now = performance.now();
|
|
4883
|
-
if (now - this.
|
|
4884
|
-
return this.
|
|
4917
|
+
if (now - this.stickyCacheTick < 16)
|
|
4918
|
+
return this.stickyCacheVal;
|
|
4885
4919
|
const sticky = this.scrollEl.querySelector(".selective-ui-option-handle:not(.hide)");
|
|
4886
|
-
this.
|
|
4887
|
-
this.
|
|
4888
|
-
return this.
|
|
4920
|
+
this.stickyCacheVal = sticky?.offsetHeight ?? 0;
|
|
4921
|
+
this.stickyCacheTick = now;
|
|
4922
|
+
return this.stickyCacheVal;
|
|
4889
4923
|
}
|
|
4890
4924
|
/** Schedules window update on next frame if not already scheduled. */
|
|
4891
4925
|
scheduleUpdateWindow() {
|
|
4892
|
-
if (this.
|
|
4926
|
+
if (this.rafId != null || this.suspended)
|
|
4893
4927
|
return;
|
|
4894
|
-
this.
|
|
4895
|
-
this.
|
|
4928
|
+
this.rafId = requestAnimationFrame(() => {
|
|
4929
|
+
this.rafId = null;
|
|
4896
4930
|
this.updateWindowInternal();
|
|
4897
4931
|
});
|
|
4898
4932
|
}
|
|
@@ -5014,10 +5048,10 @@
|
|
|
5014
5048
|
if (this.resizeObs)
|
|
5015
5049
|
return;
|
|
5016
5050
|
this.resizeObs = new ResizeObserver(() => {
|
|
5017
|
-
if (this.
|
|
5051
|
+
if (this.suppressResize || this.suspended || !this.adapter || this.measureRaf != null)
|
|
5018
5052
|
return;
|
|
5019
|
-
this.
|
|
5020
|
-
this.
|
|
5053
|
+
this.measureRaf = requestAnimationFrame(() => {
|
|
5054
|
+
this.measureRaf = null;
|
|
5021
5055
|
this.measureVisibleAndUpdate();
|
|
5022
5056
|
});
|
|
5023
5057
|
});
|
|
@@ -5067,17 +5101,17 @@
|
|
|
5067
5101
|
* 7. Adjusts scroll position to maintain anchor item position
|
|
5068
5102
|
*/
|
|
5069
5103
|
updateWindowInternal() {
|
|
5070
|
-
if (this.
|
|
5104
|
+
if (this.updating || this.suspended)
|
|
5071
5105
|
return;
|
|
5072
|
-
this.
|
|
5106
|
+
this.updating = true;
|
|
5073
5107
|
try {
|
|
5074
5108
|
if (!this.adapter)
|
|
5075
5109
|
return;
|
|
5076
5110
|
const count = this.adapter.itemCount();
|
|
5077
5111
|
if (count <= 0)
|
|
5078
5112
|
return;
|
|
5079
|
-
if (this.
|
|
5080
|
-
this.
|
|
5113
|
+
if (this.lastRenderCount !== count) {
|
|
5114
|
+
this.lastRenderCount = count;
|
|
5081
5115
|
this.heightCache.length = count;
|
|
5082
5116
|
this.rebuildFenwick(count);
|
|
5083
5117
|
}
|
|
@@ -5101,7 +5135,7 @@
|
|
|
5101
5135
|
return;
|
|
5102
5136
|
this.start = startIndex;
|
|
5103
5137
|
this.end = endIndex;
|
|
5104
|
-
this.
|
|
5138
|
+
this.suppressResize = true;
|
|
5105
5139
|
try {
|
|
5106
5140
|
this.mountRange(this.start, this.end);
|
|
5107
5141
|
this.unmountOutside(this.start, this.end);
|
|
@@ -5115,18 +5149,20 @@
|
|
|
5115
5149
|
this.PadBottom.style.height = `${bottomPx}px`;
|
|
5116
5150
|
}
|
|
5117
5151
|
finally {
|
|
5118
|
-
this.
|
|
5152
|
+
this.suppressResize = false;
|
|
5119
5153
|
}
|
|
5120
5154
|
const anchorTopNew = this.offsetTopOf(anchorIndex);
|
|
5121
5155
|
const targetScroll = this.containerTopInScroll() + anchorTopNew - anchorDelta;
|
|
5122
5156
|
const maxScroll = Math.max(0, this.scrollEl.scrollHeight - this.scrollEl.clientHeight);
|
|
5123
5157
|
const clamped = Math.min(Math.max(0, targetScroll), maxScroll);
|
|
5124
|
-
|
|
5158
|
+
const heightChanged = Math.abs(anchorTopNew - anchorTop) > 1;
|
|
5159
|
+
const scrollDiff = Math.abs(this.scrollEl.scrollTop - clamped);
|
|
5160
|
+
if (heightChanged && scrollDiff > 0.5 && scrollDiff < 100) {
|
|
5125
5161
|
this.scrollEl.scrollTop = clamped;
|
|
5126
5162
|
}
|
|
5127
5163
|
}
|
|
5128
5164
|
finally {
|
|
5129
|
-
this.
|
|
5165
|
+
this.updating = false;
|
|
5130
5166
|
}
|
|
5131
5167
|
}
|
|
5132
5168
|
/** Mounts all items in inclusive range [start..end]. */
|
|
@@ -5395,12 +5431,6 @@
|
|
|
5395
5431
|
this.isVisible = Libs.string2Boolean(dataset.visible ?? "1");
|
|
5396
5432
|
}
|
|
5397
5433
|
};
|
|
5398
|
-
// Custom event (manual refresh)
|
|
5399
|
-
select.addEventListener("options:changed", () => {
|
|
5400
|
-
optionModelManager.update(Libs.parseSelectToArray(select));
|
|
5401
|
-
this.getAction()?.refreshMask();
|
|
5402
|
-
container.popup?.triggerResize?.();
|
|
5403
|
-
});
|
|
5404
5434
|
// AJAX setup (if provided)
|
|
5405
5435
|
if (options.ajax) {
|
|
5406
5436
|
searchController.setAjax(options.ajax);
|
|
@@ -5418,6 +5448,7 @@
|
|
|
5418
5448
|
.search(keyword)
|
|
5419
5449
|
.then((result) => {
|
|
5420
5450
|
clearTimeout(hightlightTimer);
|
|
5451
|
+
Libs.callbackScheduler.clear(`sche_vis_proxy_${optionAdapter.adapterKey}`);
|
|
5421
5452
|
Libs.callbackScheduler.on(`sche_vis_proxy_${optionAdapter.adapterKey}`, () => {
|
|
5422
5453
|
container.popup?.triggerResize?.();
|
|
5423
5454
|
if (result?.hasResults) {
|
|
@@ -5875,9 +5906,9 @@
|
|
|
5875
5906
|
*/
|
|
5876
5907
|
class ElementAdditionObserver {
|
|
5877
5908
|
constructor() {
|
|
5878
|
-
this.
|
|
5879
|
-
this.
|
|
5880
|
-
this.
|
|
5909
|
+
this.isActive = false;
|
|
5910
|
+
this.observer = null;
|
|
5911
|
+
this.actions = [];
|
|
5881
5912
|
}
|
|
5882
5913
|
/**
|
|
5883
5914
|
* Registers a callback to be invoked whenever a matching element is detected being added to the DOM.
|
|
@@ -5885,13 +5916,13 @@
|
|
|
5885
5916
|
* @param {(el: T) => void} action - Function executed with the newly added element.
|
|
5886
5917
|
*/
|
|
5887
5918
|
onDetect(action) {
|
|
5888
|
-
this.
|
|
5919
|
+
this.actions.push(action);
|
|
5889
5920
|
}
|
|
5890
5921
|
/**
|
|
5891
5922
|
* Clears all previously registered detection callbacks.
|
|
5892
5923
|
*/
|
|
5893
5924
|
clearDetect() {
|
|
5894
|
-
this.
|
|
5925
|
+
this.actions = [];
|
|
5895
5926
|
}
|
|
5896
5927
|
/**
|
|
5897
5928
|
* Starts observing the document for additions of elements matching the given tag.
|
|
@@ -5900,26 +5931,26 @@
|
|
|
5900
5931
|
* @param {string} tag - The tag name to watch for (e.g., "select", "div").
|
|
5901
5932
|
*/
|
|
5902
5933
|
start(tag) {
|
|
5903
|
-
if (this.
|
|
5934
|
+
if (this.isActive)
|
|
5904
5935
|
return;
|
|
5905
|
-
this.
|
|
5936
|
+
this.isActive = true;
|
|
5906
5937
|
const upperTag = tag.toUpperCase();
|
|
5907
5938
|
const lowerTag = tag.toLowerCase();
|
|
5908
|
-
this.
|
|
5939
|
+
this.observer = new MutationObserver((mutations) => {
|
|
5909
5940
|
for (const mutation of mutations) {
|
|
5910
5941
|
mutation.addedNodes.forEach((node) => {
|
|
5911
5942
|
if (node.nodeType !== 1)
|
|
5912
5943
|
return;
|
|
5913
5944
|
const subnode = node;
|
|
5914
5945
|
if (subnode.tagName === upperTag) {
|
|
5915
|
-
this.
|
|
5946
|
+
this.handle(subnode);
|
|
5916
5947
|
}
|
|
5917
5948
|
const matches = subnode.querySelectorAll(lowerTag);
|
|
5918
|
-
matches.forEach((el) => this.
|
|
5949
|
+
matches.forEach((el) => this.handle(el));
|
|
5919
5950
|
});
|
|
5920
5951
|
}
|
|
5921
5952
|
});
|
|
5922
|
-
this.
|
|
5953
|
+
this.observer.observe(document.body, {
|
|
5923
5954
|
childList: true,
|
|
5924
5955
|
subtree: true,
|
|
5925
5956
|
});
|
|
@@ -5929,19 +5960,19 @@
|
|
|
5929
5960
|
* No-ops if the observer is not active.
|
|
5930
5961
|
*/
|
|
5931
5962
|
stop() {
|
|
5932
|
-
if (!this.
|
|
5963
|
+
if (!this.isActive)
|
|
5933
5964
|
return;
|
|
5934
|
-
this.
|
|
5935
|
-
this.
|
|
5936
|
-
this.
|
|
5965
|
+
this.isActive = false;
|
|
5966
|
+
this.observer?.disconnect();
|
|
5967
|
+
this.observer = null;
|
|
5937
5968
|
}
|
|
5938
5969
|
/**
|
|
5939
5970
|
* Internal handler that invokes all registered detection callbacks for the provided element.
|
|
5940
5971
|
*
|
|
5941
5972
|
* @param {T} element - The element that was detected as added to the DOM.
|
|
5942
5973
|
*/
|
|
5943
|
-
|
|
5944
|
-
this.
|
|
5974
|
+
handle(element) {
|
|
5975
|
+
this.actions.forEach((action) => action(element));
|
|
5945
5976
|
}
|
|
5946
5977
|
}
|
|
5947
5978
|
|
|
@@ -6256,7 +6287,7 @@
|
|
|
6256
6287
|
if (typeof globalThis.GLOBAL_SEUI == "undefined") {
|
|
6257
6288
|
const SECLASS = new Selective();
|
|
6258
6289
|
globalThis.GLOBAL_SEUI = {
|
|
6259
|
-
version: "1.2.
|
|
6290
|
+
version: "1.2.3",
|
|
6260
6291
|
name: "SelectiveUI",
|
|
6261
6292
|
bind: SECLASS.bind.bind(SECLASS),
|
|
6262
6293
|
find: SECLASS.find.bind(SECLASS),
|
|
@@ -6287,7 +6318,7 @@
|
|
|
6287
6318
|
init();
|
|
6288
6319
|
}
|
|
6289
6320
|
}
|
|
6290
|
-
console.log(`[${"SelectiveUI"}] v${"1.2.
|
|
6321
|
+
console.log(`[${"SelectiveUI"}] v${"1.2.3"} loaded successfully`);
|
|
6291
6322
|
}
|
|
6292
6323
|
else {
|
|
6293
6324
|
console.warn(`[${globalThis.GLOBAL_SEUI.name}] Already loaded (v${globalThis.GLOBAL_SEUI.version}). ` +
|