selective-ui 1.2.2 → 1.2.4
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.map +1 -1
- package/dist/selective-ui.esm.js +2178 -695
- 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.js +2 -2
- package/dist/selective-ui.min.js.br +0 -0
- package/dist/selective-ui.umd.js +2179 -696
- package/dist/selective-ui.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/ts/adapter/mixed-adapter.ts +167 -80
- package/src/ts/components/accessorybox.ts +155 -34
- package/src/ts/components/directive.ts +62 -11
- package/src/ts/components/option-handle.ts +124 -28
- package/src/ts/components/placeholder.ts +73 -16
- package/src/ts/components/popup/empty-state.ts +126 -0
- package/src/ts/components/popup/loading-state.ts +120 -0
- package/src/ts/components/{popup.ts → popup/popup.ts} +169 -72
- package/src/ts/components/searchbox.ts +82 -16
- package/src/ts/components/selectbox.ts +207 -115
- package/src/ts/core/base/adapter.ts +121 -55
- package/src/ts/core/base/lifecycle.ts +175 -0
- package/src/ts/core/base/model.ts +63 -32
- package/src/ts/core/base/recyclerview.ts +56 -18
- package/src/ts/core/base/view.ts +56 -19
- package/src/ts/core/base/virtual-recyclerview.ts +322 -128
- package/src/ts/core/model-manager.ts +18 -11
- package/src/ts/core/search-controller.ts +148 -25
- package/src/ts/global.ts +5 -5
- package/src/ts/index.ts +5 -5
- package/src/ts/models/group-model.ts +27 -6
- package/src/ts/models/option-model.ts +29 -6
- package/src/ts/services/ea-observer.ts +6 -6
- package/src/ts/services/effector.ts +2 -2
- package/src/ts/services/select-observer.ts +1 -8
- package/src/ts/types/components/searchbox.type.ts +1 -1
- package/src/ts/types/core/base/adapter.type.ts +2 -1
- package/src/ts/types/core/base/lifecycle.type.ts +62 -0
- package/src/ts/types/core/base/model.type.ts +3 -1
- package/src/ts/types/core/base/recyclerview.type.ts +2 -8
- package/src/ts/types/core/base/view.type.ts +36 -24
- package/src/ts/utils/callback-scheduler.ts +38 -18
- package/src/ts/utils/istorage.ts +1 -1
- package/src/ts/utils/selective.ts +153 -36
- package/src/ts/views/group-view.ts +59 -21
- package/src/ts/views/option-view.ts +137 -68
- package/src/ts/components/empty-state.ts +0 -68
- package/src/ts/components/loading-state.ts +0 -66
- /package/src/css/components/{empty-state.css → popup/empty-state.css} +0 -0
- /package/src/css/components/{loading-state.css → popup/loading-state.css} +0 -0
- /package/src/css/components/{popup.css → popup/popup.css} +0 -0
- /package/src/css/{components/optgroup.css → views/group-view.css} +0 -0
- /package/src/css/{components/option.css → views/option-view.css} +0 -0
|
@@ -16,7 +16,7 @@ export class SelectObserver {
|
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Initializes the SelectObserver for a given <select> element.
|
|
19
|
-
* Captures the initial snapshot, sets up a MutationObserver
|
|
19
|
+
* Captures the initial snapshot, sets up a MutationObserver.
|
|
20
20
|
* Changes are debounced to prevent excessive calls.
|
|
21
21
|
*
|
|
22
22
|
* @param {HTMLSelectElement} select - The <select> element to observe.
|
|
@@ -31,13 +31,6 @@ export class SelectObserver {
|
|
|
31
31
|
this.handleChange();
|
|
32
32
|
}, this._DEBOUNCE_DELAY);
|
|
33
33
|
});
|
|
34
|
-
|
|
35
|
-
select.addEventListener("options:changed", () => {
|
|
36
|
-
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
37
|
-
this.debounceTimer = setTimeout(() => {
|
|
38
|
-
this.handleChange();
|
|
39
|
-
}, this._DEBOUNCE_DELAY);
|
|
40
|
-
});
|
|
41
34
|
}
|
|
42
35
|
|
|
43
36
|
/**
|
|
@@ -2,7 +2,7 @@ import { PlaceHolder } from "src/ts/components/placeholder";
|
|
|
2
2
|
import { MountViewResult } from "../utils/libs.type";
|
|
3
3
|
import { Directive } from "src/ts/components/directive";
|
|
4
4
|
import { SearchBox } from "src/ts/components/searchbox";
|
|
5
|
-
import { Popup } from "src/ts/components/popup";
|
|
5
|
+
import { Popup } from "src/ts/components/popup/popup";
|
|
6
6
|
import { EffectorInterface } from "../services/effector.type";
|
|
7
7
|
import { AccessoryBox } from "src/ts/components/accessorybox";
|
|
8
8
|
import { SearchController } from "src/ts/core/search-controller";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Lifecycle } from "src/ts/core/base/lifecycle";
|
|
1
2
|
import type { ModelContract } from "./model.type";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -5,7 +6,7 @@ import type { ModelContract } from "./model.type";
|
|
|
5
6
|
*
|
|
6
7
|
* @template TItem - A model type that implements ModelContract.
|
|
7
8
|
*/
|
|
8
|
-
export interface AdapterContract<TItem extends ModelContract<any, any>> {
|
|
9
|
+
export interface AdapterContract<TItem extends ModelContract<any, any>> extends Lifecycle {
|
|
9
10
|
/**
|
|
10
11
|
* List of items managed by the adapter.
|
|
11
12
|
* These items are rendered or updated in the associated container.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* Enumerates the finite lifecycle states used across core classes (e.g., View/Model).
|
|
4
|
+
*
|
|
5
|
+
* State flow (happy path):
|
|
6
|
+
* NEW → INITIALIZED → MOUNTED → UPDATED → DESTROYED
|
|
7
|
+
*
|
|
8
|
+
* Notes:
|
|
9
|
+
* - `UPDATED` may be emitted multiple times after mount, depending on implementation.
|
|
10
|
+
* - `DESTROYED` is terminal; no further transitions should occur.
|
|
11
|
+
*/
|
|
12
|
+
export enum LifecycleState {
|
|
13
|
+
/** The instance has been created but not initialized. */
|
|
14
|
+
NEW = "new",
|
|
15
|
+
|
|
16
|
+
/** Initialization logic has completed; ready to be mounted. */
|
|
17
|
+
INITIALIZED = "initialized",
|
|
18
|
+
|
|
19
|
+
/** The instance is mounted/attached to its environment (e.g., DOM). */
|
|
20
|
+
MOUNTED = "mounted",
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The instance has been updated at least once after being mounted.
|
|
24
|
+
* Further updates typically keep the state at UPDATED.
|
|
25
|
+
*/
|
|
26
|
+
UPDATED = "updated",
|
|
27
|
+
|
|
28
|
+
/** Teardown has run; resources/hooks have been released. */
|
|
29
|
+
DESTROYED = "destroyed",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Optional callbacks for lifecycle events.
|
|
34
|
+
*
|
|
35
|
+
* Implementers can subscribe to these hooks either by:
|
|
36
|
+
* - passing these functions to a contract that consumes `LifecycleHooks`, or
|
|
37
|
+
* - using the `Lifecycle.on(hook, fn)` API at runtime.
|
|
38
|
+
*
|
|
39
|
+
* Each callback is executed without arguments and should be side-effect free
|
|
40
|
+
* except for logic relevant to the lifecycle step.
|
|
41
|
+
*/
|
|
42
|
+
export interface LifecycleHooks {
|
|
43
|
+
/** Invoked when transitioning from `NEW` → `INITIALIZED`. */
|
|
44
|
+
onInit?(): void;
|
|
45
|
+
|
|
46
|
+
/** Invoked when transitioning from `INITIALIZED` → `MOUNTED`. */
|
|
47
|
+
onMount?(): void;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Invoked when transitioning to `UPDATED`.
|
|
51
|
+
* May be emitted multiple times after the instance is mounted.
|
|
52
|
+
*/
|
|
53
|
+
onUpdate?(): void;
|
|
54
|
+
|
|
55
|
+
/** Invoked when transitioning to `DESTROYED`. Should be idempotent. */
|
|
56
|
+
onDestroy?(): void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type LifecycleHookContext = {
|
|
60
|
+
state: LifecycleState;
|
|
61
|
+
prevState: LifecycleState;
|
|
62
|
+
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { Lifecycle } from "src/ts/core/base/lifecycle";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Generic model contract for binding a target element and an optional view.
|
|
3
5
|
*
|
|
4
6
|
* @template TTarget - The type of the target element (e.g., DOM element, data object).
|
|
5
7
|
* @template TView - The type of the view associated with the target (e.g., UI component).
|
|
6
8
|
*/
|
|
7
|
-
export interface ModelContract<TTarget, TView> {
|
|
9
|
+
export interface ModelContract<TTarget, TView> extends Lifecycle {
|
|
8
10
|
/**
|
|
9
11
|
* The target element that this model is bound to.
|
|
10
12
|
* Can be null if not yet initialized.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Lifecycle } from "src/ts/core/base/lifecycle";
|
|
1
2
|
import { AdapterContract } from "./adapter.type";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -5,7 +6,7 @@ import { AdapterContract } from "./adapter.type";
|
|
|
5
6
|
*
|
|
6
7
|
* @template TAdapter - A concrete adapter type that implements AdapterContract.
|
|
7
8
|
*/
|
|
8
|
-
export interface RecyclerViewContract<TAdapter extends AdapterContract<any>> {
|
|
9
|
+
export interface RecyclerViewContract<TAdapter extends AdapterContract<any>> extends Lifecycle {
|
|
9
10
|
/**
|
|
10
11
|
* The container element that hosts all item views.
|
|
11
12
|
* Can be null until `setView` is called.
|
|
@@ -18,13 +19,6 @@ export interface RecyclerViewContract<TAdapter extends AdapterContract<any>> {
|
|
|
18
19
|
*/
|
|
19
20
|
adapter: TAdapter | null;
|
|
20
21
|
|
|
21
|
-
/**
|
|
22
|
-
* Set or replace the container element used for rendering.
|
|
23
|
-
*
|
|
24
|
-
* @param viewElement - The root container element for the recycler view.
|
|
25
|
-
*/
|
|
26
|
-
setView(viewElement: HTMLElement): void;
|
|
27
|
-
|
|
28
22
|
/**
|
|
29
23
|
* Attach an adapter and refresh the view accordingly.
|
|
30
24
|
* Implementations typically call `render()` or reconcile the DOM.
|
|
@@ -1,43 +1,55 @@
|
|
|
1
|
+
|
|
2
|
+
import { Lifecycle } from "src/ts/core/base/lifecycle";
|
|
1
3
|
import { MountViewResult } from "../../utils/libs.type";
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
|
-
* Contract for a UI
|
|
5
|
-
*
|
|
6
|
+
* Contract definition for a UI View created via `mountView` or `mountNode`.
|
|
7
|
+
*
|
|
8
|
+
* A View encapsulates:
|
|
9
|
+
* - The mounted DOM structure
|
|
10
|
+
* - The root element
|
|
11
|
+
* - A strongly-typed map of named child elements
|
|
12
|
+
* - Lifecycle management hooks
|
|
6
13
|
*
|
|
7
14
|
* @template TTags - A map of tag names to their corresponding HTMLElement instances.
|
|
8
|
-
*
|
|
15
|
+
* Example:
|
|
16
|
+
* ```ts
|
|
17
|
+
* {
|
|
18
|
+
* Root: HTMLDivElement;
|
|
19
|
+
* Button: HTMLButtonElement;
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
9
22
|
*/
|
|
10
|
-
export interface ViewContract<TTags extends Record<string, HTMLElement>> {
|
|
23
|
+
export interface ViewContract<TTags extends Record<string, HTMLElement>> extends Lifecycle {
|
|
24
|
+
|
|
11
25
|
/**
|
|
12
|
-
* The parent
|
|
13
|
-
*
|
|
26
|
+
* The parent DOM element into which the view is mounted.
|
|
27
|
+
*
|
|
28
|
+
* This value is:
|
|
29
|
+
* - `null` before the view is mounted
|
|
30
|
+
* - Set once the view is attached to the DOM
|
|
14
31
|
*/
|
|
15
32
|
parent: HTMLElement | null;
|
|
16
33
|
|
|
17
34
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
35
|
+
* Internal representation of the mounted view returned by `mountView` or `mountNode`.
|
|
36
|
+
*
|
|
37
|
+
* Contains:
|
|
38
|
+
* - The root element of the view
|
|
39
|
+
* - A strongly-typed tag map for querying child elements
|
|
40
|
+
*
|
|
41
|
+
* Will be `null` if the view has not been mounted yet.
|
|
21
42
|
*/
|
|
22
43
|
view: MountViewResult<TTags> | null;
|
|
23
44
|
|
|
24
45
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Update the view.
|
|
32
|
-
* Implementations typically refresh displayed data without a full re-render.
|
|
33
|
-
*/
|
|
34
|
-
update(): void;
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Get the root HTMLElement for the mounted view.
|
|
46
|
+
* Returns the root HTMLElement of the mounted view.
|
|
47
|
+
*
|
|
48
|
+
* This is typically the top-level container element
|
|
49
|
+
* created by `mountView` / `mountNode`.
|
|
38
50
|
*
|
|
39
|
-
* @returns The root
|
|
40
|
-
* @throws If the view
|
|
51
|
+
* @returns The root HTMLElement of the view.
|
|
52
|
+
* @throws {Error} If the view has not been mounted or initialized.
|
|
41
53
|
*/
|
|
42
54
|
getView(): HTMLElement;
|
|
43
55
|
}
|
|
@@ -84,12 +84,18 @@ export class CallbackScheduler {
|
|
|
84
84
|
* - If an entry has `once = true`, it is removed after execution by setting its slot to `undefined`.
|
|
85
85
|
* (The list is not spliced to preserve indices.)
|
|
86
86
|
*/
|
|
87
|
-
public run(key: TimerKey, ...params: any[]): void {
|
|
87
|
+
public run(key: TimerKey, ...params: any[]): Promise<void> | void {
|
|
88
88
|
const executes = this.executeStored.get(key);
|
|
89
|
-
if (!executes)
|
|
89
|
+
if (!executes || executes.length === 0) {
|
|
90
|
+
return Promise.resolve();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!this.timerRunner.has(key)) {
|
|
94
|
+
this.timerRunner.set(key, new Map());
|
|
95
|
+
}
|
|
90
96
|
|
|
91
|
-
if (!this.timerRunner.has(key)) this.timerRunner.set(key, new Map());
|
|
92
97
|
const runner = this.timerRunner.get(key)!;
|
|
98
|
+
const tasks: Promise<void>[] = [];
|
|
93
99
|
|
|
94
100
|
for (let i = 0; i < executes.length; i++) {
|
|
95
101
|
const entry = executes[i];
|
|
@@ -98,22 +104,36 @@ export class CallbackScheduler {
|
|
|
98
104
|
const prev = runner.get(i);
|
|
99
105
|
if (prev) clearTimeout(prev);
|
|
100
106
|
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
107
|
+
const task = new Promise<void>((resolve) => {
|
|
108
|
+
const timer = setTimeout(async () => {
|
|
109
|
+
try {
|
|
110
|
+
const resp = entry.callback(
|
|
111
|
+
params.length > 0 ? params : null
|
|
112
|
+
) as any;
|
|
113
|
+
|
|
114
|
+
if (resp instanceof Promise) {
|
|
115
|
+
await resp;
|
|
116
|
+
}
|
|
117
|
+
} catch {} finally {
|
|
118
|
+
if (entry.once) {
|
|
119
|
+
executes[i] = undefined;
|
|
120
|
+
|
|
121
|
+
const current = runner.get(i);
|
|
122
|
+
if (current) clearTimeout(current);
|
|
123
|
+
runner.delete(i);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
resolve();
|
|
127
|
+
}
|
|
128
|
+
}, entry.timeout);
|
|
129
|
+
|
|
130
|
+
runner.set(i, timer);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
tasks.push(task);
|
|
116
134
|
}
|
|
135
|
+
|
|
136
|
+
return Promise.all(tasks).then(() => void 0);
|
|
117
137
|
}
|
|
118
138
|
|
|
119
139
|
/**
|
package/src/ts/utils/istorage.ts
CHANGED
|
@@ -4,13 +4,31 @@ import { SelectBox } from "../components/selectbox";
|
|
|
4
4
|
import { ElementAdditionObserver } from "../services/ea-observer";
|
|
5
5
|
import { SelectiveActionApi, SelectiveOptions } from "../types/utils/selective.type";
|
|
6
6
|
import { BinderMap, PropertiesType } from "../types/utils/istorage.type";
|
|
7
|
-
import {
|
|
7
|
+
import { Lifecycle } from "../core/base/lifecycle";
|
|
8
|
+
import { LifecycleState } from "../types/core/base/lifecycle.type";
|
|
8
9
|
|
|
9
|
-
export class Selective {
|
|
10
|
+
export class Selective extends Lifecycle {
|
|
10
11
|
private EAObserver: ElementAdditionObserver;
|
|
11
12
|
|
|
12
13
|
private bindedQueries: Map<string, SelectiveOptions> = new Map();
|
|
13
14
|
|
|
15
|
+
constructor() {
|
|
16
|
+
super();
|
|
17
|
+
this.init();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Override lifecycle init - Initialize Selective system
|
|
22
|
+
*/
|
|
23
|
+
public init(): void {
|
|
24
|
+
if (!this.is(LifecycleState.NEW)) return;
|
|
25
|
+
|
|
26
|
+
// Initialize core properties
|
|
27
|
+
this.bindedQueries = new Map();
|
|
28
|
+
|
|
29
|
+
super.init();
|
|
30
|
+
}
|
|
31
|
+
|
|
14
32
|
/**
|
|
15
33
|
* Binds Selective UI to all <select> elements matching the query.
|
|
16
34
|
* Merges provided options with defaults, schedules `on.load` callbacks,
|
|
@@ -20,27 +38,40 @@ export class Selective {
|
|
|
20
38
|
* @param {object} options - Configuration overrides merged with defaults.
|
|
21
39
|
*/
|
|
22
40
|
public bind(query: string, options: SelectiveOptions): void {
|
|
23
|
-
|
|
41
|
+
// Auto-init if not initialized
|
|
42
|
+
if (this.is(LifecycleState.NEW)) {
|
|
43
|
+
this.init();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const merged = Libs.mergeConfig(
|
|
47
|
+
Libs.getDefaultConfig(),
|
|
48
|
+
options,
|
|
49
|
+
) as SelectiveOptions;
|
|
24
50
|
|
|
25
51
|
// Ensure hooks exist
|
|
26
52
|
merged.on = merged.on ?? {};
|
|
27
|
-
merged.on.load = (merged.on.load ?? []) as Array<
|
|
53
|
+
merged.on.load = (merged.on.load ?? []) as Array<
|
|
54
|
+
(...args: any[]) => void
|
|
55
|
+
>;
|
|
28
56
|
|
|
29
57
|
this.bindedQueries.set(query, merged);
|
|
30
58
|
|
|
31
59
|
const doneToken = Libs.randomString();
|
|
32
60
|
Libs.callbackScheduler.on(doneToken, () => {
|
|
33
|
-
iEvents.callEvent([this.find(query)], ...
|
|
61
|
+
iEvents.callEvent([this.find(query)], ...merged.on!.load);
|
|
34
62
|
Libs.callbackScheduler.clear(doneToken);
|
|
35
63
|
merged.on!.load = [];
|
|
36
64
|
});
|
|
37
65
|
|
|
38
66
|
const selectElements = Libs.getElements(query) as HTMLSelectElement[];
|
|
67
|
+
let hasAnyBound = false;
|
|
68
|
+
|
|
39
69
|
selectElements.forEach((item) => {
|
|
40
70
|
(async () => {
|
|
41
71
|
if (item.tagName === "SELECT") {
|
|
42
72
|
Libs.removeUnbinderMap(item);
|
|
43
73
|
if (this.applySelectBox(item, merged)) {
|
|
74
|
+
hasAnyBound = true;
|
|
44
75
|
Libs.callbackScheduler.run(doneToken);
|
|
45
76
|
}
|
|
46
77
|
}
|
|
@@ -50,6 +81,34 @@ export class Selective {
|
|
|
50
81
|
if (!Libs.getBindedCommand().includes(query)) {
|
|
51
82
|
Libs.getBindedCommand().push(query);
|
|
52
83
|
}
|
|
84
|
+
|
|
85
|
+
// Mount if first bind and has elements
|
|
86
|
+
if (this.is(LifecycleState.INITIALIZED) && hasAnyBound) {
|
|
87
|
+
this.mount();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Trigger update if already mounted
|
|
91
|
+
if (this.is(LifecycleState.MOUNTED)) {
|
|
92
|
+
this.update();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Override lifecycle mount - System is ready and has bound elements
|
|
98
|
+
*/
|
|
99
|
+
public mount(): void {
|
|
100
|
+
if (this.state !== LifecycleState.INITIALIZED) return;
|
|
101
|
+
|
|
102
|
+
super.mount();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Override lifecycle update - Called when bindings change
|
|
107
|
+
*/
|
|
108
|
+
public update(): void {
|
|
109
|
+
if (this.state !== LifecycleState.MOUNTED) return;
|
|
110
|
+
|
|
111
|
+
super.update();
|
|
53
112
|
}
|
|
54
113
|
|
|
55
114
|
/**
|
|
@@ -101,19 +160,27 @@ export class Selective {
|
|
|
101
160
|
* Selective bindings automatically when they match previously bound queries.
|
|
102
161
|
*/
|
|
103
162
|
public Observer(): void {
|
|
104
|
-
this.EAObserver
|
|
105
|
-
|
|
106
|
-
this.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
163
|
+
if (!this.EAObserver) {
|
|
164
|
+
this.EAObserver = new ElementAdditionObserver();
|
|
165
|
+
this.EAObserver.onDetect((selectElement: HTMLSelectElement) => {
|
|
166
|
+
this.bindedQueries.forEach((options, query) => {
|
|
167
|
+
try {
|
|
168
|
+
if (selectElement.matches(query)) {
|
|
169
|
+
this.applySelectBox(selectElement, options);
|
|
170
|
+
|
|
171
|
+
// Trigger update when new element is bound
|
|
172
|
+
if (this.is(LifecycleState.MOUNTED)) {
|
|
173
|
+
this.update();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
} catch (error) {
|
|
177
|
+
console.warn(`Invalid selector: ${query}`, error);
|
|
110
178
|
}
|
|
111
|
-
}
|
|
112
|
-
console.warn(`Invalid selector: ${query}`, error);
|
|
113
|
-
}
|
|
179
|
+
});
|
|
114
180
|
});
|
|
115
|
-
}
|
|
116
|
-
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
this.EAObserver.connect("select");
|
|
117
184
|
}
|
|
118
185
|
|
|
119
186
|
/**
|
|
@@ -132,19 +199,30 @@ export class Selective {
|
|
|
132
199
|
} else if (target instanceof HTMLSelectElement) {
|
|
133
200
|
this.destroyElement(target);
|
|
134
201
|
}
|
|
202
|
+
|
|
203
|
+
// Trigger update after partial destroy
|
|
204
|
+
if (target !== null && this.is(LifecycleState.MOUNTED)) {
|
|
205
|
+
this.update();
|
|
206
|
+
}
|
|
135
207
|
}
|
|
136
208
|
|
|
137
209
|
/**
|
|
138
210
|
* Destroys all bound Selective instances and clears bindings/state.
|
|
139
211
|
* Stops the ElementAdditionObserver.
|
|
212
|
+
* Calls lifecycle destroy.
|
|
140
213
|
*/
|
|
141
214
|
private destroyAll(): void {
|
|
215
|
+
if (this.state === LifecycleState.DESTROYED) return;
|
|
216
|
+
|
|
142
217
|
const bindedCommands = Libs.getBindedCommand();
|
|
143
218
|
bindedCommands.forEach((query: string) => this.destroyByQuery(query));
|
|
144
219
|
|
|
145
220
|
this.bindedQueries.clear();
|
|
146
221
|
Libs.getBindedCommand().length = 0;
|
|
147
|
-
this.EAObserver?.
|
|
222
|
+
this.EAObserver?.disconnect();
|
|
223
|
+
|
|
224
|
+
// Call parent lifecycle destroy
|
|
225
|
+
super.destroy();
|
|
148
226
|
}
|
|
149
227
|
|
|
150
228
|
/**
|
|
@@ -176,20 +254,20 @@ export class Selective {
|
|
|
176
254
|
const bindMap = Libs.getBinderMap(selectElement) as BinderMap | null;
|
|
177
255
|
if (!bindMap) return;
|
|
178
256
|
|
|
179
|
-
const
|
|
180
|
-
popup?.detroy();
|
|
257
|
+
const selfBox = bindMap.self as SelectBox | null;
|
|
181
258
|
|
|
182
259
|
Libs.setUnbinderMap(selectElement, bindMap);
|
|
183
260
|
|
|
184
261
|
const wasObserving = !!this.EAObserver;
|
|
185
|
-
if (wasObserving) this.EAObserver?.
|
|
262
|
+
if (wasObserving) this.EAObserver?.disconnect();
|
|
186
263
|
|
|
187
264
|
try {
|
|
188
265
|
bindMap.self?.deInit?.();
|
|
189
|
-
} catch (_) {
|
|
266
|
+
} catch (_) {}
|
|
190
267
|
|
|
191
268
|
const wrapper: HTMLElement | null =
|
|
192
|
-
(bindMap.container?.element as HTMLElement | undefined) ??
|
|
269
|
+
(bindMap.container?.element as HTMLElement | undefined) ??
|
|
270
|
+
selectElement.parentElement;
|
|
193
271
|
|
|
194
272
|
selectElement.style.display = "";
|
|
195
273
|
selectElement.style.visibility = "";
|
|
@@ -199,14 +277,16 @@ export class Selective {
|
|
|
199
277
|
if (wrapper && wrapper.parentNode) {
|
|
200
278
|
wrapper.parentNode.replaceChild(selectElement, wrapper);
|
|
201
279
|
} else {
|
|
202
|
-
|
|
280
|
+
selectElement.appendChild(selectElement);
|
|
203
281
|
}
|
|
204
282
|
|
|
205
283
|
Libs.removeBinderMap(selectElement);
|
|
206
284
|
|
|
207
285
|
if (wasObserving && this.bindedQueries.size > 0) {
|
|
208
|
-
this.EAObserver?.
|
|
286
|
+
this.EAObserver?.connect("select");
|
|
209
287
|
}
|
|
288
|
+
|
|
289
|
+
selfBox?.destroy?.();
|
|
210
290
|
}
|
|
211
291
|
|
|
212
292
|
/**
|
|
@@ -219,6 +299,11 @@ export class Selective {
|
|
|
219
299
|
public rebind(query: string, options: SelectiveOptions): void {
|
|
220
300
|
this.destroyByQuery(query);
|
|
221
301
|
this.bind(query, options);
|
|
302
|
+
|
|
303
|
+
// Trigger update after rebind
|
|
304
|
+
if (this.is(LifecycleState.MOUNTED)) {
|
|
305
|
+
this.update();
|
|
306
|
+
}
|
|
222
307
|
}
|
|
223
308
|
|
|
224
309
|
/**
|
|
@@ -230,13 +315,22 @@ export class Selective {
|
|
|
230
315
|
* @param {object} options - Configuration used for this instance.
|
|
231
316
|
* @returns {boolean} - False if already bound; true if successfully applied.
|
|
232
317
|
*/
|
|
233
|
-
private applySelectBox(
|
|
234
|
-
|
|
318
|
+
private applySelectBox(
|
|
319
|
+
selectElement: HTMLSelectElement,
|
|
320
|
+
options: SelectiveOptions,
|
|
321
|
+
): boolean {
|
|
322
|
+
if (
|
|
323
|
+
Libs.getBinderMap(selectElement) ||
|
|
324
|
+
Libs.getUnbinderMap(selectElement)
|
|
325
|
+
) {
|
|
235
326
|
return false;
|
|
236
327
|
}
|
|
237
328
|
|
|
238
329
|
const SEID = Libs.randomString(8);
|
|
239
|
-
const options_cfg = Libs.buildConfig(
|
|
330
|
+
const options_cfg = Libs.buildConfig(
|
|
331
|
+
selectElement,
|
|
332
|
+
options,
|
|
333
|
+
) as SelectiveOptions;
|
|
240
334
|
|
|
241
335
|
options_cfg.SEID = SEID;
|
|
242
336
|
options_cfg.SEID_LIST = `seui-${SEID}-optionlist`;
|
|
@@ -245,15 +339,24 @@ export class Selective {
|
|
|
245
339
|
const bindMap: BinderMap = { options: options_cfg };
|
|
246
340
|
Libs.setBinderMap(selectElement, bindMap);
|
|
247
341
|
|
|
342
|
+
// Create SelectBox with lifecycle
|
|
248
343
|
const selectBox = new SelectBox(selectElement, this);
|
|
344
|
+
|
|
345
|
+
selectBox.on("onMount", () => {
|
|
346
|
+
if (selectBox.container.view) {
|
|
347
|
+
selectBox.container.view.addEventListener("mouseup", () => {
|
|
348
|
+
bindMap.action?.toggle?.();
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// Mount the SelectBox
|
|
354
|
+
selectBox.mount();
|
|
355
|
+
|
|
249
356
|
bindMap.container = selectBox.container;
|
|
250
357
|
bindMap.action = selectBox.getAction();
|
|
251
358
|
bindMap.self = selectBox;
|
|
252
359
|
|
|
253
|
-
selectBox.container.view.addEventListener("mouseup", () => {
|
|
254
|
-
bindMap.action?.toggle?.();
|
|
255
|
-
});
|
|
256
|
-
|
|
257
360
|
return true;
|
|
258
361
|
}
|
|
259
362
|
|
|
@@ -268,11 +371,17 @@ export class Selective {
|
|
|
268
371
|
* @param {*} action - The object containing the property.
|
|
269
372
|
* @returns {PropertiesType} - The derived property type and name.
|
|
270
373
|
*/
|
|
271
|
-
private getProperties(
|
|
374
|
+
private getProperties(
|
|
375
|
+
actionName: string,
|
|
376
|
+
action: Record<string, any>,
|
|
377
|
+
): PropertiesType {
|
|
272
378
|
const descriptor = Object.getOwnPropertyDescriptor(action, actionName);
|
|
273
379
|
let type: PropertiesType["type"] = "variable";
|
|
274
380
|
|
|
275
|
-
if (
|
|
381
|
+
if (
|
|
382
|
+
descriptor?.get ||
|
|
383
|
+
(descriptor?.set && typeof action[actionName] !== "function")
|
|
384
|
+
) {
|
|
276
385
|
type = "get-set";
|
|
277
386
|
} else if (typeof action[actionName] === "function") {
|
|
278
387
|
type = "func";
|
|
@@ -289,7 +398,11 @@ export class Selective {
|
|
|
289
398
|
* @param {string} name - The property name to expose.
|
|
290
399
|
* @param {HTMLElement[]} els - The list of bound elements to proxy.
|
|
291
400
|
*/
|
|
292
|
-
private buildGetSetAction(
|
|
401
|
+
private buildGetSetAction(
|
|
402
|
+
object: Record<string, any>,
|
|
403
|
+
name: string,
|
|
404
|
+
els: HTMLElement[],
|
|
405
|
+
): void {
|
|
293
406
|
Object.defineProperty(object, name, {
|
|
294
407
|
get() {
|
|
295
408
|
const binded = Libs.getBinderMap(els[0]) as BinderMap;
|
|
@@ -315,7 +428,11 @@ export class Selective {
|
|
|
315
428
|
* @param {string} name - The function name to expose.
|
|
316
429
|
* @param {HTMLElement[]} els - The list of bound elements to invoke against.
|
|
317
430
|
*/
|
|
318
|
-
private buildFuntionAction(
|
|
431
|
+
private buildFuntionAction(
|
|
432
|
+
object: Record<string, any>,
|
|
433
|
+
name: string,
|
|
434
|
+
els: HTMLElement[],
|
|
435
|
+
): void {
|
|
319
436
|
object[name] = (...params: any[]) => {
|
|
320
437
|
let resp = null;
|
|
321
438
|
for (let index = 0; index < els.length; index++) {
|
|
@@ -331,4 +448,4 @@ export class Selective {
|
|
|
331
448
|
return resp ?? object;
|
|
332
449
|
};
|
|
333
450
|
}
|
|
334
|
-
}
|
|
451
|
+
}
|