selective-ui 1.4.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/selective-ui.css +0 -6
- package/dist/selective-ui.css.map +1 -1
- package/dist/selective-ui.esm.js +252 -553
- 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 +254 -555
- package/dist/selective-ui.umd.js.map +1 -1
- package/package.json +12 -12
- package/src/ts/adapter/mixed-adapter.ts +147 -68
- package/src/ts/components/accessorybox.ts +14 -11
- package/src/ts/components/directive.ts +1 -1
- package/src/ts/components/option-handle.ts +12 -9
- package/src/ts/components/placeholder.ts +5 -5
- package/src/ts/components/popup/empty-state.ts +5 -5
- package/src/ts/components/popup/loading-state.ts +5 -5
- package/src/ts/components/popup/popup.ts +138 -76
- package/src/ts/components/searchbox.ts +17 -13
- package/src/ts/components/selectbox.ts +242 -81
- package/src/ts/core/base/adapter.ts +39 -14
- package/src/ts/core/base/fenwick.ts +3 -2
- package/src/ts/core/base/lifecycle.ts +14 -4
- package/src/ts/core/base/model.ts +17 -15
- package/src/ts/core/base/recyclerview.ts +7 -5
- package/src/ts/core/base/view.ts +10 -5
- package/src/ts/core/base/virtual-recyclerview.ts +89 -37
- package/src/ts/core/model-manager.ts +48 -21
- package/src/ts/core/search-controller.ts +174 -56
- package/src/ts/global.ts +5 -8
- package/src/ts/index.ts +2 -2
- package/src/ts/models/group-model.ts +33 -8
- package/src/ts/models/option-model.ts +60 -19
- package/src/ts/services/dataset-observer.ts +6 -3
- package/src/ts/services/ea-observer.ts +1 -1
- package/src/ts/services/effector.ts +22 -12
- package/src/ts/services/refresher.ts +7 -3
- package/src/ts/services/resize-observer.ts +24 -11
- package/src/ts/services/select-observer.ts +2 -2
- package/src/ts/types/components/popup.type.ts +18 -1
- package/src/ts/types/components/searchbox.type.ts +43 -30
- package/src/ts/types/components/state.box.type.ts +1 -1
- package/src/ts/types/core/base/adapter.type.ts +13 -5
- package/src/ts/types/core/base/lifecycle.type.ts +1 -2
- package/src/ts/types/core/base/model.type.ts +3 -3
- package/src/ts/types/core/base/recyclerview.type.ts +7 -5
- package/src/ts/types/core/base/view.type.ts +6 -6
- package/src/ts/types/core/base/virtual-recyclerview.type.ts +45 -46
- package/src/ts/types/core/search-controller.type.ts +18 -2
- package/src/ts/types/css.d.ts +1 -0
- package/src/ts/types/plugins/plugin.type.ts +2 -2
- package/src/ts/types/services/effector.type.ts +25 -25
- package/src/ts/types/services/resize-observer.type.ts +23 -12
- package/src/ts/types/utils/callback-scheduler.type.ts +2 -2
- package/src/ts/types/utils/ievents.type.ts +1 -1
- package/src/ts/types/utils/istorage.type.ts +62 -60
- package/src/ts/types/utils/libs.type.ts +19 -17
- package/src/ts/types/utils/selective.type.ts +6 -3
- package/src/ts/types/views/view.group.type.ts +9 -5
- package/src/ts/types/views/view.option.type.ts +39 -17
- package/src/ts/utils/callback-scheduler.ts +12 -7
- package/src/ts/utils/ievents.ts +12 -5
- package/src/ts/utils/istorage.ts +5 -3
- package/src/ts/utils/libs.ts +122 -43
- package/src/ts/utils/selective.ts +15 -8
- package/src/ts/views/group-view.ts +11 -9
- package/src/ts/views/option-view.ts +37 -18
|
@@ -50,17 +50,19 @@ import { Lifecycle } from "./base/lifecycle";
|
|
|
50
50
|
*/
|
|
51
51
|
export class ModelManager<
|
|
52
52
|
TModel extends MixedItem,
|
|
53
|
-
TAdapter extends Adapter<MixedItem, ViewContract<any
|
|
53
|
+
TAdapter extends Adapter<MixedItem, ViewContract<any>>,
|
|
54
54
|
> extends Lifecycle {
|
|
55
55
|
private privModelList: Array<MixedItem> = [];
|
|
56
56
|
|
|
57
57
|
private privAdapter!: new (...args: any[]) => TAdapter;
|
|
58
58
|
|
|
59
|
-
private privAdapterHandle
|
|
59
|
+
private privAdapterHandle?: TAdapter;
|
|
60
60
|
|
|
61
|
-
private privRecyclerView!: new (
|
|
61
|
+
private privRecyclerView!: new (
|
|
62
|
+
...args: any[]
|
|
63
|
+
) => RecyclerViewContract<TAdapter>;
|
|
62
64
|
|
|
63
|
-
private privRecyclerViewHandle
|
|
65
|
+
private privRecyclerViewHandle?: RecyclerViewContract<TAdapter>;
|
|
64
66
|
|
|
65
67
|
private options: SelectiveOptions = null;
|
|
66
68
|
|
|
@@ -97,7 +99,9 @@ export class ModelManager<
|
|
|
97
99
|
* @param {new (...args: any[]) => RecyclerViewContract<TAdapter>} recyclerView - The recycler view constructor.
|
|
98
100
|
* @returns {void}
|
|
99
101
|
*/
|
|
100
|
-
public setupRecyclerView(
|
|
102
|
+
public setupRecyclerView(
|
|
103
|
+
recyclerView: new (...args: any[]) => RecyclerViewContract<TAdapter>,
|
|
104
|
+
): void {
|
|
101
105
|
this.privRecyclerView = recyclerView;
|
|
102
106
|
}
|
|
103
107
|
|
|
@@ -113,7 +117,9 @@ export class ModelManager<
|
|
|
113
117
|
* @param {Array<HTMLOptGroupElement | HTMLOptionElement>} modelData - Parsed DOM elements from the source `<select>`.
|
|
114
118
|
* @returns {Array<GroupModel | OptionModel>} The ordered list of group and option models.
|
|
115
119
|
*/
|
|
116
|
-
public createModelResources(
|
|
120
|
+
public createModelResources(
|
|
121
|
+
modelData: Array<HTMLOptGroupElement | HTMLOptionElement>,
|
|
122
|
+
): Array<GroupModel | OptionModel> {
|
|
117
123
|
if (this.is(LifecycleState.INITIALIZED)) {
|
|
118
124
|
this.mount();
|
|
119
125
|
}
|
|
@@ -123,14 +129,26 @@ export class ModelManager<
|
|
|
123
129
|
|
|
124
130
|
modelData.forEach((data) => {
|
|
125
131
|
if (data.tagName === "OPTGROUP") {
|
|
126
|
-
currentGroup = new GroupModel(
|
|
132
|
+
currentGroup = new GroupModel(
|
|
133
|
+
this.options,
|
|
134
|
+
data as HTMLOptGroupElement,
|
|
135
|
+
);
|
|
127
136
|
this.privModelList.push(currentGroup);
|
|
128
137
|
} else if (data.tagName === "OPTION") {
|
|
129
|
-
const optionModel = new OptionModel(
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
138
|
+
const optionModel = new OptionModel(
|
|
139
|
+
this.options,
|
|
140
|
+
data as HTMLOptionElement,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const parentGroup = data["__parentGroup"] as
|
|
144
|
+
| HTMLOptGroupElement
|
|
145
|
+
| undefined;
|
|
146
|
+
|
|
147
|
+
if (
|
|
148
|
+
parentGroup &&
|
|
149
|
+
currentGroup &&
|
|
150
|
+
parentGroup === currentGroup.targetElement
|
|
151
|
+
) {
|
|
134
152
|
currentGroup.addItem(optionModel);
|
|
135
153
|
optionModel.group = currentGroup;
|
|
136
154
|
} else {
|
|
@@ -155,7 +173,9 @@ export class ModelManager<
|
|
|
155
173
|
* @returns {Promise<void>} Resolves when the adapter (if any) completes syncing.
|
|
156
174
|
* @see Adapter#syncFromSource
|
|
157
175
|
*/
|
|
158
|
-
public async replace(
|
|
176
|
+
public async replace(
|
|
177
|
+
modelData: Array<HTMLOptGroupElement | HTMLOptionElement>,
|
|
178
|
+
): Promise<void> {
|
|
159
179
|
this.createModelResources(modelData);
|
|
160
180
|
|
|
161
181
|
if (this.privAdapterHandle) {
|
|
@@ -199,9 +219,9 @@ export class ModelManager<
|
|
|
199
219
|
public load<TExtra extends object = {}>(
|
|
200
220
|
viewElement: HTMLElement,
|
|
201
221
|
adapterOpt: Partial<TAdapter> = {},
|
|
202
|
-
recyclerViewOpt: Partial<RecyclerViewContract<TAdapter>> &
|
|
222
|
+
recyclerViewOpt: Partial<RecyclerViewContract<TAdapter>> &
|
|
223
|
+
TExtra = {} as any,
|
|
203
224
|
): void {
|
|
204
|
-
|
|
205
225
|
this.privAdapterHandle = new this.privAdapter(this.privModelList);
|
|
206
226
|
Object.assign(this.privAdapterHandle, adapterOpt);
|
|
207
227
|
|
|
@@ -230,7 +250,9 @@ export class ModelManager<
|
|
|
230
250
|
* @returns {void}
|
|
231
251
|
* @see Adapter#updateData
|
|
232
252
|
*/
|
|
233
|
-
public updateModel(
|
|
253
|
+
public updateModel(
|
|
254
|
+
modelData: Array<HTMLOptGroupElement | HTMLOptionElement>,
|
|
255
|
+
): void {
|
|
234
256
|
const oldModels = this.privModelList;
|
|
235
257
|
const newModels: Array<MixedItem> = [];
|
|
236
258
|
|
|
@@ -256,9 +278,10 @@ export class ModelManager<
|
|
|
256
278
|
|
|
257
279
|
if (existingGroup) {
|
|
258
280
|
// Label is used as key; keep original behavior.
|
|
259
|
-
const hasLabelChange =
|
|
281
|
+
const hasLabelChange =
|
|
282
|
+
existingGroup.label !== dataVset.label;
|
|
260
283
|
if (hasLabelChange) {
|
|
261
|
-
existingGroup.updateTarget(dataVset)
|
|
284
|
+
existingGroup.updateTarget(dataVset);
|
|
262
285
|
}
|
|
263
286
|
|
|
264
287
|
existingGroup.position = position;
|
|
@@ -283,7 +306,9 @@ export class ModelManager<
|
|
|
283
306
|
existingOption.updateTarget(dataVset);
|
|
284
307
|
existingOption.position = position;
|
|
285
308
|
|
|
286
|
-
const parentGroup = dataVset[
|
|
309
|
+
const parentGroup = dataVset[
|
|
310
|
+
"__parentGroup"
|
|
311
|
+
] as HTMLOptGroupElement;
|
|
287
312
|
|
|
288
313
|
if (parentGroup && currentGroup) {
|
|
289
314
|
currentGroup.addItem(existingOption);
|
|
@@ -298,7 +323,9 @@ export class ModelManager<
|
|
|
298
323
|
const newOption = new OptionModel(this.options, dataVset);
|
|
299
324
|
newOption.position = position;
|
|
300
325
|
|
|
301
|
-
const parentGroup = dataVset[
|
|
326
|
+
const parentGroup = dataVset[
|
|
327
|
+
"__parentGroup"
|
|
328
|
+
] as HTMLOptGroupElement;
|
|
302
329
|
|
|
303
330
|
if (parentGroup && currentGroup) {
|
|
304
331
|
currentGroup.addItem(newOption);
|
|
@@ -438,4 +465,4 @@ export class ModelManager<
|
|
|
438
465
|
public triggerChanged(event_name: string): Promise<void> {
|
|
439
466
|
return this.privAdapterHandle?.changeProp(event_name);
|
|
440
467
|
}
|
|
441
|
-
}
|
|
468
|
+
}
|
|
@@ -4,7 +4,12 @@ import { GroupModel } from "../models/group-model";
|
|
|
4
4
|
import { OptionModel } from "../models/option-model";
|
|
5
5
|
import { LifecycleState } from "../types/core/base/lifecycle.type";
|
|
6
6
|
import { MixedItem } from "../types/core/base/mixed-adapter.type";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
AjaxConfig,
|
|
9
|
+
NormalizedAjaxItem,
|
|
10
|
+
PaginationState,
|
|
11
|
+
ParseResponseResult,
|
|
12
|
+
} from "../types/core/search-controller.type";
|
|
8
13
|
import { Libs } from "../utils/libs";
|
|
9
14
|
import { Lifecycle } from "./base/lifecycle";
|
|
10
15
|
import { ModelManager } from "./model-manager";
|
|
@@ -52,13 +57,13 @@ export class SearchController extends Lifecycle {
|
|
|
52
57
|
* AJAX configuration; when `null`, {@link search} falls back to local filtering.
|
|
53
58
|
* @see {@link setAjax}
|
|
54
59
|
*/
|
|
55
|
-
private ajaxConfig
|
|
60
|
+
private ajaxConfig?: AjaxConfig;
|
|
56
61
|
|
|
57
62
|
/** Abort handle used to cancel an in-flight AJAX request when a newer request starts. */
|
|
58
|
-
private abortController
|
|
63
|
+
private abortController?: AbortController;
|
|
59
64
|
|
|
60
65
|
/** Optional popup handle used for showing/hiding loading UI during remote operations. */
|
|
61
|
-
private popup
|
|
66
|
+
private popup?: Popup;
|
|
62
67
|
|
|
63
68
|
/**
|
|
64
69
|
* SelectBox handle used by custom data builder functions that require Selective context.
|
|
@@ -88,7 +93,11 @@ export class SearchController extends Lifecycle {
|
|
|
88
93
|
* @param {ModelManager<MixedItem, any>} modelManager - Manager responsible for model resources and rendering refresh.
|
|
89
94
|
* @param {SelectBox} selectBox - SelectBox handle used by configured AJAX data builders.
|
|
90
95
|
*/
|
|
91
|
-
public constructor(
|
|
96
|
+
public constructor(
|
|
97
|
+
selectElement: HTMLSelectElement,
|
|
98
|
+
modelManager: ModelManager<MixedItem, any>,
|
|
99
|
+
selectBox: SelectBox,
|
|
100
|
+
) {
|
|
92
101
|
super();
|
|
93
102
|
this.initialize(selectElement, modelManager, selectBox);
|
|
94
103
|
}
|
|
@@ -102,7 +111,11 @@ export class SearchController extends Lifecycle {
|
|
|
102
111
|
* @param {SelectBox} selectBox - SelectBox handle.
|
|
103
112
|
* @returns {void}
|
|
104
113
|
*/
|
|
105
|
-
private initialize(
|
|
114
|
+
private initialize(
|
|
115
|
+
selectElement: HTMLSelectElement,
|
|
116
|
+
modelManager: ModelManager<MixedItem, any>,
|
|
117
|
+
selectBox: SelectBox,
|
|
118
|
+
): void {
|
|
106
119
|
this.select = selectElement;
|
|
107
120
|
this.modelManager = modelManager;
|
|
108
121
|
this.selectBox = selectBox;
|
|
@@ -137,9 +150,17 @@ export class SearchController extends Lifecycle {
|
|
|
137
150
|
* - When AJAX is not configured, resolves with `{ success: false, ... }`.
|
|
138
151
|
* - This method does not mutate the `<select>`; it only returns normalized items.
|
|
139
152
|
*/
|
|
140
|
-
async loadByValues(values: string | string[]): Promise<{
|
|
153
|
+
async loadByValues(values: string | string[]): Promise<{
|
|
154
|
+
success: boolean;
|
|
155
|
+
items: NormalizedAjaxItem[];
|
|
156
|
+
message?: string;
|
|
157
|
+
}> {
|
|
141
158
|
if (!this.ajaxConfig) {
|
|
142
|
-
return {
|
|
159
|
+
return {
|
|
160
|
+
success: false,
|
|
161
|
+
items: [],
|
|
162
|
+
message: "Ajax not configured",
|
|
163
|
+
};
|
|
143
164
|
}
|
|
144
165
|
|
|
145
166
|
const valuesArray = Array.isArray(values) ? values : [values];
|
|
@@ -156,8 +177,12 @@ export class SearchController extends Lifecycle {
|
|
|
156
177
|
values: valuesArray.join(","),
|
|
157
178
|
load_by_values: "1",
|
|
158
179
|
...(typeof cfg.data === "function"
|
|
159
|
-
? cfg.data.bind(
|
|
160
|
-
|
|
180
|
+
? cfg.data.bind(
|
|
181
|
+
this.selectBox.Selective.find(
|
|
182
|
+
this.selectBox.container.targetElement,
|
|
183
|
+
),
|
|
184
|
+
)("", 0)
|
|
185
|
+
: (cfg.data ?? {})),
|
|
161
186
|
};
|
|
162
187
|
}
|
|
163
188
|
|
|
@@ -165,18 +190,23 @@ export class SearchController extends Lifecycle {
|
|
|
165
190
|
|
|
166
191
|
if ((cfg.method ?? "GET") === "POST") {
|
|
167
192
|
const formData = new URLSearchParams();
|
|
168
|
-
Object.keys(payload).forEach((key) =>
|
|
193
|
+
Object.keys(payload).forEach((key) =>
|
|
194
|
+
formData.append(key, String(payload[key])),
|
|
195
|
+
);
|
|
169
196
|
response = await fetch(cfg.url, {
|
|
170
197
|
method: "POST",
|
|
171
198
|
body: formData,
|
|
172
|
-
headers: {
|
|
199
|
+
headers: {
|
|
200
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
201
|
+
},
|
|
173
202
|
});
|
|
174
203
|
} else {
|
|
175
204
|
const params = new URLSearchParams(payload).toString();
|
|
176
205
|
response = await fetch(`${cfg.url}?${params}`);
|
|
177
206
|
}
|
|
178
207
|
|
|
179
|
-
if (!response.ok)
|
|
208
|
+
if (!response.ok)
|
|
209
|
+
throw new Error(`HTTP error! status: ${response.status}`);
|
|
180
210
|
|
|
181
211
|
const data = await response.json();
|
|
182
212
|
const result = this.parseResponse(data);
|
|
@@ -197,7 +227,10 @@ export class SearchController extends Lifecycle {
|
|
|
197
227
|
* @param {string[]} values - Values to check.
|
|
198
228
|
* @returns {{ existing: string[]; missing: string[] }} Partitioned result.
|
|
199
229
|
*/
|
|
200
|
-
public checkMissingValues(values: string[]): {
|
|
230
|
+
public checkMissingValues(values: string[]): {
|
|
231
|
+
existing: string[];
|
|
232
|
+
missing: string[];
|
|
233
|
+
} {
|
|
201
234
|
const allOptions = Array.from(this.select.options);
|
|
202
235
|
const existingValues = allOptions.map((opt) => opt.value);
|
|
203
236
|
|
|
@@ -211,10 +244,10 @@ export class SearchController extends Lifecycle {
|
|
|
211
244
|
* Configures AJAX settings used for remote searching and pagination.
|
|
212
245
|
* Setting `null` disables AJAX mode and causes {@link search} to use local filtering.
|
|
213
246
|
*
|
|
214
|
-
* @param {AjaxConfig
|
|
247
|
+
* @param {AjaxConfig} config - AJAX configuration (endpoint, method, data builders, keepSelected, ...).
|
|
215
248
|
* @returns {void}
|
|
216
249
|
*/
|
|
217
|
-
public setAjax(config
|
|
250
|
+
public setAjax(config?: AjaxConfig): void {
|
|
218
251
|
this.ajaxConfig = config;
|
|
219
252
|
}
|
|
220
253
|
|
|
@@ -272,7 +305,8 @@ export class SearchController extends Lifecycle {
|
|
|
272
305
|
|
|
273
306
|
for (const m of modelList) {
|
|
274
307
|
if (m instanceof OptionModel) flatOptions.push(m);
|
|
275
|
-
else if (m instanceof GroupModel && Array.isArray(m.items))
|
|
308
|
+
else if (m instanceof GroupModel && Array.isArray(m.items))
|
|
309
|
+
flatOptions.push(...m.items);
|
|
276
310
|
}
|
|
277
311
|
|
|
278
312
|
flatOptions.forEach((opt) => {
|
|
@@ -289,7 +323,10 @@ export class SearchController extends Lifecycle {
|
|
|
289
323
|
* @param {boolean} [append=false] - AJAX mode only: append results (next page) instead of replacing.
|
|
290
324
|
* @returns {Promise<any>} Implementation-specific result object from the underlying strategy.
|
|
291
325
|
*/
|
|
292
|
-
public async search(
|
|
326
|
+
public async search(
|
|
327
|
+
keyword: string,
|
|
328
|
+
append: boolean = false,
|
|
329
|
+
): Promise<any> {
|
|
293
330
|
if (this.ajaxConfig) return this.ajaxSearch(keyword, append);
|
|
294
331
|
return this.localSearch(keyword);
|
|
295
332
|
}
|
|
@@ -305,10 +342,14 @@ export class SearchController extends Lifecycle {
|
|
|
305
342
|
* @returns {Promise<any>} Result of the paginated request, or an error object when not applicable.
|
|
306
343
|
*/
|
|
307
344
|
public async loadMore(): Promise<any> {
|
|
308
|
-
if (!this.ajaxConfig)
|
|
309
|
-
|
|
310
|
-
if (
|
|
311
|
-
|
|
345
|
+
if (!this.ajaxConfig)
|
|
346
|
+
return { success: false, message: "Ajax not enabled" };
|
|
347
|
+
if (this.paginationState.isLoading)
|
|
348
|
+
return { success: false, message: "Already loading" };
|
|
349
|
+
if (!this.paginationState.isPaginationEnabled)
|
|
350
|
+
return { success: false, message: "Pagination not enabled" };
|
|
351
|
+
if (!this.paginationState.hasMore)
|
|
352
|
+
return { success: false, message: "No more data" };
|
|
312
353
|
|
|
313
354
|
this.paginationState.currentPage++;
|
|
314
355
|
return this.ajaxSearch(this.paginationState.currentKeyword, true);
|
|
@@ -329,8 +370,11 @@ export class SearchController extends Lifecycle {
|
|
|
329
370
|
* @returns {Promise<{ success: boolean; hasResults: boolean; isEmpty: boolean }>}
|
|
330
371
|
* Summary result for UI consumers.
|
|
331
372
|
*/
|
|
332
|
-
private async localSearch(
|
|
333
|
-
|
|
373
|
+
private async localSearch(
|
|
374
|
+
keyword: string,
|
|
375
|
+
): Promise<{ success: boolean; hasResults: boolean; isEmpty: boolean }> {
|
|
376
|
+
if (this.compareSearchTrigger(keyword))
|
|
377
|
+
this.paginationState.currentKeyword = keyword;
|
|
334
378
|
|
|
335
379
|
const lower = String(keyword ?? "").toLowerCase();
|
|
336
380
|
const lowerNA = Libs.string2normalize(lower);
|
|
@@ -340,7 +384,8 @@ export class SearchController extends Lifecycle {
|
|
|
340
384
|
const flatOptions: OptionModel[] = [];
|
|
341
385
|
for (const m of modelList) {
|
|
342
386
|
if (m instanceof OptionModel) flatOptions.push(m);
|
|
343
|
-
else if (m instanceof GroupModel && Array.isArray(m.items))
|
|
387
|
+
else if (m instanceof GroupModel && Array.isArray(m.items))
|
|
388
|
+
flatOptions.push(...m.items);
|
|
344
389
|
}
|
|
345
390
|
|
|
346
391
|
let hasVisibleItems = false;
|
|
@@ -388,7 +433,10 @@ export class SearchController extends Lifecycle {
|
|
|
388
433
|
* @param {boolean} [append=false] - Whether to append results (true = next page).
|
|
389
434
|
* @returns {Promise<any>} Implementation-specific result object with pagination flags.
|
|
390
435
|
*/
|
|
391
|
-
private async ajaxSearch(
|
|
436
|
+
private async ajaxSearch(
|
|
437
|
+
keyword: string,
|
|
438
|
+
append: boolean = false,
|
|
439
|
+
): Promise<any> {
|
|
392
440
|
const cfg = this.ajaxConfig!;
|
|
393
441
|
if (this.compareSearchTrigger(keyword)) {
|
|
394
442
|
this.resetPagination();
|
|
@@ -410,11 +458,19 @@ export class SearchController extends Lifecycle {
|
|
|
410
458
|
|
|
411
459
|
let payload: Record<string, any>;
|
|
412
460
|
if (typeof cfg.data === "function") {
|
|
413
|
-
const selectiveInstance = this.selectBox?.Selective?.find(
|
|
461
|
+
const selectiveInstance = this.selectBox?.Selective?.find(
|
|
462
|
+
this.selectBox?.container?.targetElement,
|
|
463
|
+
);
|
|
414
464
|
payload = cfg.data.call(selectiveInstance, keyword, page);
|
|
415
|
-
if (payload && typeof payload.selectedValue === "undefined")
|
|
465
|
+
if (payload && typeof payload.selectedValue === "undefined")
|
|
466
|
+
payload.selectedValue = selectedValues;
|
|
416
467
|
} else {
|
|
417
|
-
payload = {
|
|
468
|
+
payload = {
|
|
469
|
+
search: keyword,
|
|
470
|
+
page,
|
|
471
|
+
selectedValue: selectedValues,
|
|
472
|
+
...(cfg.data ?? {}),
|
|
473
|
+
};
|
|
418
474
|
}
|
|
419
475
|
|
|
420
476
|
try {
|
|
@@ -422,16 +478,22 @@ export class SearchController extends Lifecycle {
|
|
|
422
478
|
|
|
423
479
|
if ((cfg.method ?? "GET") === "POST") {
|
|
424
480
|
const formData = new URLSearchParams();
|
|
425
|
-
Object.keys(payload).forEach((key) =>
|
|
481
|
+
Object.keys(payload).forEach((key) =>
|
|
482
|
+
formData.append(key, String(payload[key])),
|
|
483
|
+
);
|
|
426
484
|
response = await fetch(cfg.url, {
|
|
427
485
|
method: "POST",
|
|
428
486
|
body: formData,
|
|
429
|
-
headers: {
|
|
487
|
+
headers: {
|
|
488
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
489
|
+
},
|
|
430
490
|
signal: this.abortController.signal,
|
|
431
491
|
});
|
|
432
492
|
} else {
|
|
433
493
|
const params = new URLSearchParams(payload).toString();
|
|
434
|
-
response = await fetch(`${cfg.url}?${params}`, {
|
|
494
|
+
response = await fetch(`${cfg.url}?${params}`, {
|
|
495
|
+
signal: this.abortController.signal,
|
|
496
|
+
});
|
|
435
497
|
}
|
|
436
498
|
|
|
437
499
|
const data = await response.json();
|
|
@@ -466,7 +528,8 @@ export class SearchController extends Lifecycle {
|
|
|
466
528
|
this.paginationState.isLoading = false;
|
|
467
529
|
this.popup?.hideLoading();
|
|
468
530
|
|
|
469
|
-
if (error?.name === "AbortError")
|
|
531
|
+
if (error?.name === "AbortError")
|
|
532
|
+
return { success: false, message: "Request aborted" };
|
|
470
533
|
|
|
471
534
|
console.error("Ajax search error:", error);
|
|
472
535
|
return { success: false, message: error?.message };
|
|
@@ -502,7 +565,10 @@ export class SearchController extends Lifecycle {
|
|
|
502
565
|
if (typeof data.page !== "undefined") {
|
|
503
566
|
hasPagination = true;
|
|
504
567
|
page = parseInt(data.page ?? 0, 10);
|
|
505
|
-
totalPages = parseInt(
|
|
568
|
+
totalPages = parseInt(
|
|
569
|
+
data.totalPages ?? data.total_page ?? 1,
|
|
570
|
+
10,
|
|
571
|
+
);
|
|
506
572
|
hasMore = page < totalPages - 1;
|
|
507
573
|
}
|
|
508
574
|
} else if (data.data && Array.isArray(data.data)) {
|
|
@@ -510,8 +576,11 @@ export class SearchController extends Lifecycle {
|
|
|
510
576
|
if (typeof data.page !== "undefined") {
|
|
511
577
|
hasPagination = true;
|
|
512
578
|
page = parseInt(data.page ?? 0, 10);
|
|
513
|
-
totalPages = parseInt(
|
|
514
|
-
|
|
579
|
+
totalPages = parseInt(
|
|
580
|
+
data.totalPages ?? data.total_page ?? 1,
|
|
581
|
+
10,
|
|
582
|
+
);
|
|
583
|
+
hasMore = data.hasMore ?? page < totalPages - 1;
|
|
515
584
|
}
|
|
516
585
|
} else if (Array.isArray(data)) {
|
|
517
586
|
items = data;
|
|
@@ -520,25 +589,53 @@ export class SearchController extends Lifecycle {
|
|
|
520
589
|
if (data.pagination) {
|
|
521
590
|
hasPagination = true;
|
|
522
591
|
page = parseInt(data.pagination.page ?? 0, 10);
|
|
523
|
-
totalPages = parseInt(
|
|
524
|
-
|
|
592
|
+
totalPages = parseInt(
|
|
593
|
+
data.pagination.totalPages ??
|
|
594
|
+
data.pagination.total_page ??
|
|
595
|
+
1,
|
|
596
|
+
10,
|
|
597
|
+
);
|
|
598
|
+
hasMore = data.pagination.hasMore ?? page < totalPages - 1;
|
|
525
599
|
}
|
|
526
600
|
}
|
|
527
601
|
|
|
528
602
|
const normalized: NormalizedAjaxItem[] = items.map((item: any) => {
|
|
529
|
-
if (
|
|
530
|
-
|
|
531
|
-
|
|
603
|
+
if (
|
|
604
|
+
item instanceof HTMLOptionElement ||
|
|
605
|
+
item instanceof HTMLOptGroupElement
|
|
606
|
+
)
|
|
607
|
+
return item;
|
|
608
|
+
|
|
609
|
+
if (
|
|
610
|
+
item.type === "optgroup" ||
|
|
611
|
+
item.isGroup ||
|
|
612
|
+
item.group ||
|
|
613
|
+
item.label
|
|
614
|
+
) {
|
|
532
615
|
const label = item.label ?? item.name ?? item.title ?? "";
|
|
533
616
|
const dataObj = item.data ?? {};
|
|
534
|
-
const opts = (item.options ?? item.items ?? []).map(
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
617
|
+
const opts = (item.options ?? item.items ?? []).map(
|
|
618
|
+
(opt: any) => ({
|
|
619
|
+
value: opt.value ?? opt.id ?? opt.key ?? "",
|
|
620
|
+
text:
|
|
621
|
+
opt.text ??
|
|
622
|
+
opt.label ??
|
|
623
|
+
opt.name ??
|
|
624
|
+
opt.title ??
|
|
625
|
+
"",
|
|
626
|
+
selected: opt.selected ?? false,
|
|
627
|
+
data:
|
|
628
|
+
opt.data ??
|
|
629
|
+
(opt.imgsrc ? { imgsrc: opt.imgsrc } : {}),
|
|
630
|
+
}),
|
|
631
|
+
);
|
|
632
|
+
|
|
633
|
+
return {
|
|
634
|
+
type: "optgroup",
|
|
635
|
+
label,
|
|
636
|
+
data: dataObj,
|
|
637
|
+
options: opts,
|
|
638
|
+
};
|
|
542
639
|
}
|
|
543
640
|
|
|
544
641
|
const dataObj = item.data ?? {};
|
|
@@ -574,19 +671,34 @@ export class SearchController extends Lifecycle {
|
|
|
574
671
|
* @param {boolean} [append=false] - Append to existing options instead of replacing.
|
|
575
672
|
* @returns {void}
|
|
576
673
|
*/
|
|
577
|
-
public applyAjaxResult(
|
|
674
|
+
public applyAjaxResult(
|
|
675
|
+
items: NormalizedAjaxItem[],
|
|
676
|
+
keepSelected: boolean,
|
|
677
|
+
append: boolean = false,
|
|
678
|
+
): void {
|
|
578
679
|
const select = this.select;
|
|
579
680
|
|
|
580
681
|
let oldSelected: string[] = [];
|
|
581
|
-
if (keepSelected)
|
|
682
|
+
if (keepSelected)
|
|
683
|
+
oldSelected = Array.from(select.selectedOptions).map(
|
|
684
|
+
(o) => o.value,
|
|
685
|
+
);
|
|
582
686
|
|
|
583
687
|
if (!append) select.innerHTML = "";
|
|
584
688
|
|
|
585
689
|
items.forEach((item: any) => {
|
|
586
690
|
// Skip empty item (defensive guard)
|
|
587
|
-
if (
|
|
691
|
+
if (
|
|
692
|
+
(item["type"] === "option" || !item["type"]) &&
|
|
693
|
+
item["value"] === "" &&
|
|
694
|
+
item["text"] === ""
|
|
695
|
+
)
|
|
696
|
+
return;
|
|
588
697
|
|
|
589
|
-
if (
|
|
698
|
+
if (
|
|
699
|
+
item instanceof HTMLOptionElement ||
|
|
700
|
+
item instanceof HTMLOptGroupElement
|
|
701
|
+
) {
|
|
590
702
|
select.appendChild(item);
|
|
591
703
|
return;
|
|
592
704
|
}
|
|
@@ -613,7 +725,10 @@ export class SearchController extends Lifecycle {
|
|
|
613
725
|
});
|
|
614
726
|
}
|
|
615
727
|
|
|
616
|
-
if (
|
|
728
|
+
if (
|
|
729
|
+
opt.selected ||
|
|
730
|
+
(keepSelected && oldSelected.includes(option.value))
|
|
731
|
+
) {
|
|
617
732
|
option.selected = true;
|
|
618
733
|
}
|
|
619
734
|
|
|
@@ -633,7 +748,10 @@ export class SearchController extends Lifecycle {
|
|
|
633
748
|
});
|
|
634
749
|
}
|
|
635
750
|
|
|
636
|
-
if (
|
|
751
|
+
if (
|
|
752
|
+
item.selected ||
|
|
753
|
+
(keepSelected && oldSelected.includes(option.value))
|
|
754
|
+
) {
|
|
637
755
|
option.selected = true;
|
|
638
756
|
}
|
|
639
757
|
|
|
@@ -667,4 +785,4 @@ export class SearchController extends Lifecycle {
|
|
|
667
785
|
|
|
668
786
|
super.destroy();
|
|
669
787
|
}
|
|
670
|
-
}
|
|
788
|
+
}
|
package/src/ts/global.ts
CHANGED
|
@@ -57,7 +57,7 @@ if (typeof globalThis.GLOBAL_SEUI == "undefined") {
|
|
|
57
57
|
effector: Effector.bind(Effector),
|
|
58
58
|
rebind: SECLASS.rebind.bind(SECLASS),
|
|
59
59
|
registerPlugin: SECLASS.registerPlugin.bind(SECLASS),
|
|
60
|
-
unregisterPlugin: SECLASS.unregisterPlugin.bind(SECLASS)
|
|
60
|
+
unregisterPlugin: SECLASS.unregisterPlugin.bind(SECLASS),
|
|
61
61
|
} as SelectiveUIGlobal;
|
|
62
62
|
|
|
63
63
|
let domInitialized = false;
|
|
@@ -68,9 +68,7 @@ if (typeof globalThis.GLOBAL_SEUI == "undefined") {
|
|
|
68
68
|
document.addEventListener("mousedown", () => {
|
|
69
69
|
const sels = Libs.getBindedCommand();
|
|
70
70
|
if (sels.length > 0) {
|
|
71
|
-
const actionApi = SECLASS.find(
|
|
72
|
-
sels.join(", ")
|
|
73
|
-
);
|
|
71
|
+
const actionApi = SECLASS.find(sels.join(", "));
|
|
74
72
|
if (!actionApi.isEmpty) actionApi.close();
|
|
75
73
|
}
|
|
76
74
|
});
|
|
@@ -86,11 +84,10 @@ if (typeof globalThis.GLOBAL_SEUI == "undefined") {
|
|
|
86
84
|
}
|
|
87
85
|
}
|
|
88
86
|
console.log(`[${__LIB_NAME__}] v${__LIB_VERSION__} loaded successfully`);
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
87
|
+
} else {
|
|
91
88
|
console.warn(
|
|
92
89
|
`[${globalThis.GLOBAL_SEUI.name}] Already loaded (v${globalThis.GLOBAL_SEUI.version}). ` +
|
|
93
|
-
|
|
90
|
+
`Using existing instance. Please remove duplicate <script> tags.`,
|
|
94
91
|
);
|
|
95
92
|
}
|
|
96
93
|
|
|
@@ -149,7 +146,7 @@ export function find(query: string): SelectiveActionApi {
|
|
|
149
146
|
* // Destroy all instances
|
|
150
147
|
* destroy();
|
|
151
148
|
*/
|
|
152
|
-
export function destroy(query
|
|
149
|
+
export function destroy(query?: string): void {
|
|
153
150
|
globalThis.GLOBAL_SEUI.destroy(query);
|
|
154
151
|
}
|
|
155
152
|
|
package/src/ts/index.ts
CHANGED
|
@@ -98,7 +98,7 @@ export function find(query: string): SelectiveActionApi {
|
|
|
98
98
|
* // Destroy all instances
|
|
99
99
|
* destroy();
|
|
100
100
|
*/
|
|
101
|
-
export function destroy(query
|
|
101
|
+
export function destroy(query?: string): void {
|
|
102
102
|
SECLASS.destroy(query);
|
|
103
103
|
}
|
|
104
104
|
|
|
@@ -161,7 +161,7 @@ function init(): void {
|
|
|
161
161
|
const sels = Libs.getBindedCommand();
|
|
162
162
|
if (sels.length > 0) {
|
|
163
163
|
const actionApi = SECLASS.find(
|
|
164
|
-
sels.join(", ")
|
|
164
|
+
sels.join(", "),
|
|
165
165
|
) as SelectiveActionApi;
|
|
166
166
|
if (!actionApi.isEmpty) actionApi.close();
|
|
167
167
|
}
|