import { Libs } from "./libs"; import { iEvents } from "./ievents"; import { SelectBox } from "../components/selectbox"; import { ElementAdditionObserver } from "../services/ea-observer"; import { SelectiveActionApi, SelectiveOptions, } from "../types/utils/selective.type"; import { BinderMap, PropertiesType } from "../types/utils/istorage.type"; import { Lifecycle } from "../core/base/lifecycle"; import { LifecycleState } from "../types/core/base/lifecycle.type"; import { SelectivePlugin } from "../types/plugins/plugin.type"; import { SelectBoxTags } from "../types/components/searchbox.type"; /** * Selective * * Core orchestrator for the Selective UI library, managing lifecycle and bindings for enhanced ``, {@link applySelectBox} creates a {@link SelectBox} instance. * 3. Binder map stores `{ options, container, action, self }` via {@link Libs.setBinderMap}. * 4. `on.load` callbacks are scheduled and invoked after binding completes. * * ### Observer pattern * - **{@link Observer}**: Activates {@link ElementAdditionObserver} to detect new `` elements, hides originals (`display/visibility`). * - {@link destroyElement} restores original DOM structure (unwraps, re-shows select). * - {@link Observer} mutates DOM via `MutationObserver` callbacks. * * @extends Lifecycle * @see {@link SelectBox} * @see {@link ElementAdditionObserver} * @see {@link SelectiveActionApi} */ export class Selective extends Lifecycle { /** * Observer for detecting newly added `` elements matching the query. * * Binding flow: * 1. Auto-initializes if in {@link LifecycleState.NEW}. * 2. Merges `options` with defaults via {@link Libs.mergeConfig}. * 3. Registers query in {@link bindedQueries}. * 4. Schedules `on.load` callbacks (invoked after binding completes). * 5. For each matching `` elements. * @param {SelectiveOptions} options - Configuration overrides merged with defaults. * @returns {void} */ public bind(query: string, options: SelectiveOptions): void { // Auto-init if not initialized if (this.is(LifecycleState.NEW)) { this.init(); } const merged = Libs.mergeConfig( Libs.getDefaultConfig(), options, ); // Ensure hooks exist merged.on = merged.on ?? {}; merged.on.load = (merged.on.load ?? []) as Array< (...args: any[]) => void >; this.bindedQueries.set(query, merged); const doneToken = Libs.randomString(); Libs.callbackScheduler.on(doneToken, () => { iEvents.callEvent([this.find(query)], ...merged.on!.load); Libs.callbackScheduler.clear(doneToken); merged.on!.load = []; }); const selectElements = Libs.getElements(query); let hasAnyBound = false; selectElements.forEach((item) => { (async () => { if (item.tagName === "SELECT") { Libs.removeUnbinderMap(item); if (this.applySelectBox(item, merged)) { hasAnyBound = true; Libs.callbackScheduler.run(doneToken); } } })(); }); if (!Libs.getBindedCommand().includes(query)) { Libs.getBindedCommand().push(query); } // Mount if first bind and has elements if (this.is(LifecycleState.INITIALIZED) && hasAnyBound) { this.mount(); } // Trigger update if already mounted if (this.is(LifecycleState.MOUNTED)) { this.update(); } } /** * Transitions system to {@link LifecycleState.MOUNTED} state. * * Behavior: * - No-op if not in {@link LifecycleState.INITIALIZED} (strict FSM guard). * - Transitions `INITIALIZED → MOUNTED` via `super.mount()`. * * Notes: * - Typically triggered by {@link bind} on first successful binding. * - Indicates system has active bound elements and is ready for interactions. * * @public * @returns {void} * @override */ public mount(): void { if (this.state !== LifecycleState.INITIALIZED) return; super.mount(); } /** * Signals a binding change has occurred. * * Behavior: * - No-op if not in {@link LifecycleState.MOUNTED} (strict FSM guard). * - Transitions `MOUNTED → UPDATED → MOUNTED` via `super.update()`. * * Triggered by: * - {@link bind} (when already mounted). * - {@link Observer} (new element auto-bound). * - {@link destroy} (partial teardown with remaining bindings). * - {@link rebind} (query re-registration). * * @public * @returns {void} * @override */ public update(): void { if (this.state !== LifecycleState.MOUNTED) return; super.update(); } /** * Finds and returns an aggregated action API for bound `` elements. * * Behavior: * - Creates {@link ElementAdditionObserver} (if not already initialized). * - Registers callback to detect `` element. * 2. Removes query from {@link bindedQueries}. * 3. Removes query from {@link Libs.getBindedCommand}. * * Notes: * - Does **not** trigger {@link update}; caller is responsible. * - Safe to call on non-existent queries (no-op). * * @private * @param {string} query - CSS selector whose instances should be destroyed. * @returns {void} */ private destroyByQuery(query: string): void { const selectElements = Libs.getElements(query); selectElements.forEach((element) => { if (element.tagName === "SELECT") this.destroyElement(element); }); this.bindedQueries.delete(query); const commands = Libs.getBindedCommand(); const index = commands.indexOf(query); if (index > -1) commands.splice(index, 1); } /** * Destroys a single Selective instance and restores the native `` from its container (restores to original parent). * 6. Restores `` into original position. * - Resets inline styles and dataset. * * Notes: * - Safe to call on unbound elements (no-op if binder map missing). * - Errors in `deInit()` are silently caught. * * @private * @param {HTMLSelectElement} selectElement - Target `` element. * * Application flow: * 1. Guards against duplicate bindings (checks binder/unbinder maps). * 2. Generates unique instance ID (SEID) and element IDs. * 3. Merges element `dataset` into options via {@link Libs.buildConfig}. * 4. Stores initial binder map with options. * 5. Creates {@link SelectBox} instance, wires `onMount` handler for UI interactions. * 6. Calls `SelectBox.mount()` to activate lifecycle. * 7. Stores final binder map with `{ container, action, self }`. * * `onMount` behavior: * - Wires `mouseup` event on view to toggle dropdown via `bindMap.action.toggle()`. * * Return value: * - `false` if element already bound (no-op). * - `true` if successfully applied. * * @private * @param {HTMLSelectElement} selectElement - Native `