ts-util-core 2.0.0

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.
Files changed (63) hide show
  1. package/README.md +411 -0
  2. package/dist/core/ajax.d.ts +30 -0
  3. package/dist/core/ajax.d.ts.map +1 -0
  4. package/dist/core/ajax.js +110 -0
  5. package/dist/core/ajax.js.map +1 -0
  6. package/dist/core/event-emitter.d.ts +29 -0
  7. package/dist/core/event-emitter.d.ts.map +1 -0
  8. package/dist/core/event-emitter.js +67 -0
  9. package/dist/core/event-emitter.js.map +1 -0
  10. package/dist/core/message.d.ts +28 -0
  11. package/dist/core/message.d.ts.map +1 -0
  12. package/dist/core/message.js +172 -0
  13. package/dist/core/message.js.map +1 -0
  14. package/dist/core/view.d.ts +49 -0
  15. package/dist/core/view.d.ts.map +1 -0
  16. package/dist/core/view.js +87 -0
  17. package/dist/core/view.js.map +1 -0
  18. package/dist/formatting/formatters.d.ts +9 -0
  19. package/dist/formatting/formatters.d.ts.map +1 -0
  20. package/dist/formatting/formatters.js +109 -0
  21. package/dist/formatting/formatters.js.map +1 -0
  22. package/dist/formatting/registry.d.ts +31 -0
  23. package/dist/formatting/registry.d.ts.map +1 -0
  24. package/dist/formatting/registry.js +49 -0
  25. package/dist/formatting/registry.js.map +1 -0
  26. package/dist/index.d.ts +31 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +104 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/types.d.ts +66 -0
  31. package/dist/types.d.ts.map +1 -0
  32. package/dist/types.js +5 -0
  33. package/dist/types.js.map +1 -0
  34. package/dist/utils/dom.d.ts +38 -0
  35. package/dist/utils/dom.d.ts.map +1 -0
  36. package/dist/utils/dom.js +95 -0
  37. package/dist/utils/dom.js.map +1 -0
  38. package/dist/utils/sprintf.d.ts +16 -0
  39. package/dist/utils/sprintf.d.ts.map +1 -0
  40. package/dist/utils/sprintf.js +116 -0
  41. package/dist/utils/sprintf.js.map +1 -0
  42. package/dist/validation/constraints.d.ts +23 -0
  43. package/dist/validation/constraints.d.ts.map +1 -0
  44. package/dist/validation/constraints.js +131 -0
  45. package/dist/validation/constraints.js.map +1 -0
  46. package/dist/validation/validator.d.ts +45 -0
  47. package/dist/validation/validator.d.ts.map +1 -0
  48. package/dist/validation/validator.js +210 -0
  49. package/dist/validation/validator.js.map +1 -0
  50. package/package.json +26 -0
  51. package/readme.txt +4 -0
  52. package/src/core/ajax.ts +127 -0
  53. package/src/core/event-emitter.ts +84 -0
  54. package/src/core/message.ts +212 -0
  55. package/src/core/view.ts +101 -0
  56. package/src/formatting/formatters.ts +118 -0
  57. package/src/formatting/registry.ts +53 -0
  58. package/src/index.ts +142 -0
  59. package/src/types.ts +85 -0
  60. package/src/utils/dom.ts +105 -0
  61. package/src/utils/sprintf.ts +141 -0
  62. package/src/validation/constraints.ts +168 -0
  63. package/src/validation/validator.ts +276 -0
@@ -0,0 +1,212 @@
1
+ // ---------------------------------------------------------------------------
2
+ // MSG module — vanilla DOM dialog system (no 3rd-party dependency)
3
+ //
4
+ // Design patterns used:
5
+ // - Facade Pattern : simple `info()`, `modal()`, `confirm()` API over
6
+ // raw DOM creation + CSS styling
7
+ // - Strategy Pattern : appearance is controlled via CSS classes, consumers
8
+ // can override styling without touching this module
9
+ // ---------------------------------------------------------------------------
10
+
11
+ import type { MessageOptions } from '../types.js';
12
+
13
+ const OVERLAY_CLASS = 'rs-msg-overlay';
14
+ const DIALOG_CLASS = 'rs-msg-dialog';
15
+ const TITLE_CLASS = 'rs-msg-title';
16
+ const BODY_CLASS = 'rs-msg-body';
17
+ const BUTTON_BAR_CLASS = 'rs-msg-buttons';
18
+ const BUTTON_CLASS = 'rs-msg-btn';
19
+
20
+ let injectedStyles = false;
21
+
22
+ function injectStyles(): void {
23
+ if (injectedStyles) return;
24
+ injectedStyles = true;
25
+
26
+ const css = `
27
+ .${OVERLAY_CLASS} {
28
+ position: fixed; inset: 0;
29
+ background: rgba(0,0,0,0.4);
30
+ display: flex; align-items: center; justify-content: center;
31
+ z-index: 10000;
32
+ animation: rs-fade-in 0.15s ease-out;
33
+ }
34
+ .${DIALOG_CLASS} {
35
+ background: #fff; border-radius: 8px;
36
+ box-shadow: 0 8px 32px rgba(0,0,0,0.25);
37
+ min-width: 300px; max-width: 520px;
38
+ padding: 0; overflow: hidden;
39
+ animation: rs-slide-up 0.2s ease-out;
40
+ }
41
+ .${TITLE_CLASS} {
42
+ padding: 16px 20px; font-weight: 600; font-size: 15px;
43
+ border-bottom: 1px solid #eee;
44
+ }
45
+ .${BODY_CLASS} {
46
+ padding: 20px; font-size: 14px; line-height: 1.6;
47
+ }
48
+ .${BUTTON_BAR_CLASS} {
49
+ padding: 12px 20px; display: flex; justify-content: flex-end; gap: 8px;
50
+ border-top: 1px solid #eee;
51
+ }
52
+ .${BUTTON_CLASS} {
53
+ padding: 8px 18px; border: 1px solid #ccc; border-radius: 4px;
54
+ background: #fff; cursor: pointer; font-size: 14px;
55
+ transition: background 0.15s;
56
+ }
57
+ .${BUTTON_CLASS}:hover { background: #f5f5f5; }
58
+ .${BUTTON_CLASS}[data-primary] {
59
+ background: #2563eb; color: #fff; border-color: #2563eb;
60
+ }
61
+ .${BUTTON_CLASS}[data-primary]:hover { background: #1d4ed8; }
62
+ @keyframes rs-fade-in { from { opacity: 0; } to { opacity: 1; } }
63
+ @keyframes rs-slide-up {
64
+ from { transform: translateY(20px); opacity: 0; }
65
+ to { transform: translateY(0); opacity: 1; }
66
+ }
67
+ `;
68
+
69
+ const style = document.createElement('style');
70
+ style.textContent = css;
71
+ document.head.appendChild(style);
72
+ }
73
+
74
+ interface DialogHandle {
75
+ close: () => void;
76
+ element: HTMLElement;
77
+ }
78
+
79
+ function createDialog(
80
+ message: string,
81
+ options: {
82
+ title?: string;
83
+ buttons?: { label: string; value: string; primary?: boolean }[];
84
+ onButton?: (value: string) => void;
85
+ modal?: boolean;
86
+ } = {},
87
+ ): DialogHandle {
88
+ injectStyles();
89
+
90
+ const overlay = document.createElement('div');
91
+ overlay.className = OVERLAY_CLASS;
92
+
93
+ const dialog = document.createElement('div');
94
+ dialog.className = DIALOG_CLASS;
95
+ dialog.setAttribute('role', 'dialog');
96
+ dialog.setAttribute('aria-modal', String(!!options.modal));
97
+
98
+ // Title
99
+ if (options.title) {
100
+ const titleEl = document.createElement('div');
101
+ titleEl.className = TITLE_CLASS;
102
+ titleEl.textContent = options.title;
103
+ dialog.appendChild(titleEl);
104
+ }
105
+
106
+ // Body
107
+ const body = document.createElement('div');
108
+ body.className = BODY_CLASS;
109
+ body.innerHTML = message;
110
+ dialog.appendChild(body);
111
+
112
+ // Buttons
113
+ if (options.buttons && options.buttons.length > 0) {
114
+ const bar = document.createElement('div');
115
+ bar.className = BUTTON_BAR_CLASS;
116
+
117
+ for (const btn of options.buttons) {
118
+ const el = document.createElement('button');
119
+ el.className = BUTTON_CLASS;
120
+ el.textContent = btn.label;
121
+ if (btn.primary) el.setAttribute('data-primary', '');
122
+ el.addEventListener('click', () => {
123
+ options.onButton?.(btn.value);
124
+ close();
125
+ });
126
+ bar.appendChild(el);
127
+ }
128
+ dialog.appendChild(bar);
129
+ }
130
+
131
+ overlay.appendChild(dialog);
132
+ document.body.appendChild(overlay);
133
+
134
+ // Close on overlay click (if not modal)
135
+ if (!options.modal) {
136
+ overlay.addEventListener('click', (e) => {
137
+ if (e.target === overlay) close();
138
+ });
139
+ }
140
+
141
+ function close(): void {
142
+ overlay.remove();
143
+ }
144
+
145
+ return { close, element: overlay };
146
+ }
147
+
148
+ export class Message {
149
+ private currentModal: DialogHandle | null = null;
150
+
151
+ /**
152
+ * Show a brief informational message. Auto-closes after `options.autoclose` ms.
153
+ */
154
+ info(message: string, options: MessageOptions = {}): DialogHandle {
155
+ const { autoclose = 5000, title } = options;
156
+
157
+ const handle = createDialog(message, { title });
158
+
159
+ if (autoclose > 0) {
160
+ setTimeout(() => handle.close(), autoclose);
161
+ }
162
+
163
+ return handle;
164
+ }
165
+
166
+ /**
167
+ * Show a modal dialog that must be explicitly dismissed.
168
+ */
169
+ modal(message: string, options: MessageOptions = {}): DialogHandle {
170
+ this.dismissModal();
171
+
172
+ const handle = createDialog(message, {
173
+ title: options.title,
174
+ modal: true,
175
+ buttons: [{ label: 'OK', value: 'ok', primary: true }],
176
+ });
177
+
178
+ this.currentModal = handle;
179
+ return handle;
180
+ }
181
+
182
+ /**
183
+ * Show a Yes/No confirmation dialog. Executes `onConfirm` if "Yes" is chosen.
184
+ *
185
+ * @returns A `DialogHandle` for programmatic control.
186
+ */
187
+ confirm(
188
+ title: string,
189
+ message: string,
190
+ onConfirm: () => void,
191
+ ): DialogHandle {
192
+ return createDialog(message, {
193
+ title,
194
+ modal: true,
195
+ buttons: [
196
+ { label: 'Yes', value: 'Y', primary: true },
197
+ { label: 'No', value: 'N' },
198
+ ],
199
+ onButton: (val) => {
200
+ if (val === 'Y') onConfirm();
201
+ },
202
+ });
203
+ }
204
+
205
+ /**
206
+ * Close the currently open modal (if any).
207
+ */
208
+ dismissModal(): void {
209
+ this.currentModal?.close();
210
+ this.currentModal = null;
211
+ }
212
+ }
@@ -0,0 +1,101 @@
1
+ // ---------------------------------------------------------------------------
2
+ // VIEW module — load HTML fragments via AJAX and initialize them
3
+ //
4
+ // Design patterns used:
5
+ // - Observer Pattern : `beforeLoad` hooks run on every loaded fragment
6
+ // - Facade Pattern : `load()` handles fetch → parse → init → inject
7
+ // ---------------------------------------------------------------------------
8
+
9
+ import type { EventEmitter } from './event-emitter.js';
10
+ import type { AppEventMap, ViewLoadParams } from '../types.js';
11
+ import type { Ajax } from './ajax.js';
12
+ import { parseHTML } from '../utils/dom.js';
13
+
14
+ export type BeforeLoadHook = (context: HTMLElement) => void;
15
+
16
+ export class View {
17
+ private emitter: EventEmitter<AppEventMap>;
18
+ private ajax: Ajax;
19
+ private beforeLoadHooks: BeforeLoadHook[] = [];
20
+
21
+ constructor(emitter: EventEmitter<AppEventMap>, ajax: Ajax) {
22
+ this.emitter = emitter;
23
+ this.ajax = ajax;
24
+ }
25
+
26
+ /**
27
+ * Register a function that runs on every DOM fragment before it is inserted.
28
+ *
29
+ * This is the **Observer pattern** — modules self-register their
30
+ * initialization logic without the core knowing about them.
31
+ *
32
+ * @returns An unregister function.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * // Validation module registers itself
37
+ * VIEW.addBeforeLoad((context) => {
38
+ * context.querySelectorAll('[constraint~="date"]').forEach(setupDatepicker);
39
+ * });
40
+ *
41
+ * // Format module registers itself
42
+ * VIEW.addBeforeLoad((context) => {
43
+ * context.querySelectorAll('[format]').forEach(applyMask);
44
+ * });
45
+ * ```
46
+ */
47
+ addBeforeLoad(hook: BeforeLoadHook): () => void {
48
+ this.beforeLoadHooks.push(hook);
49
+ return () => {
50
+ const idx = this.beforeLoadHooks.indexOf(hook);
51
+ if (idx !== -1) this.beforeLoadHooks.splice(idx, 1);
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Run all registered `beforeLoad` hooks on a DOM element.
57
+ * Also emits the `view:beforeLoad` event for event-based listeners.
58
+ */
59
+ invokeBeforeLoad(context: HTMLElement): void {
60
+ for (const hook of this.beforeLoadHooks) {
61
+ hook(context);
62
+ }
63
+ this.emitter.emit('view:beforeLoad', { context });
64
+ }
65
+
66
+ /**
67
+ * Fetch HTML from a URL, run all beforeLoad hooks, then inject into `target`.
68
+ *
69
+ * @param target - The container element to inject into.
70
+ * @param params - Request parameters (url, data, form, etc.).
71
+ */
72
+ async load(target: HTMLElement, params: ViewLoadParams): Promise<void> {
73
+ const { success, ...requestParams } = params;
74
+
75
+ await this.ajax.request({
76
+ ...requestParams,
77
+ success: async (data) => {
78
+ const response = data as Response;
79
+ const html = await response.text();
80
+ const fragment = parseHTML(html);
81
+
82
+ // Wrap in a container so hooks can querySelector on it
83
+ const wrapper = document.createElement('div');
84
+ wrapper.appendChild(fragment);
85
+
86
+ this.invokeBeforeLoad(wrapper);
87
+ target.innerHTML = '';
88
+ target.appendChild(wrapper);
89
+
90
+ success?.(wrapper);
91
+ },
92
+ });
93
+ }
94
+
95
+ /**
96
+ * Initialize the entire page body — should be called once on DOMContentLoaded.
97
+ */
98
+ initPage(): void {
99
+ this.invokeBeforeLoad(document.body);
100
+ }
101
+ }
@@ -0,0 +1,118 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Built-in formatters — vanilla input masking (no 3rd-party dependency)
3
+ //
4
+ // Replaces jquery.maskedinput with a lightweight mask engine.
5
+ //
6
+ // Design patterns used:
7
+ // - Strategy Pattern : each formatter is an independent strategy
8
+ // - Template Method : all formatters share the same `applyMask` core,
9
+ // only the mask definition varies
10
+ // ---------------------------------------------------------------------------
11
+
12
+ import type { Formatter } from '../types.js';
13
+
14
+ /**
15
+ * Mask characters:
16
+ * `9` → digit (0-9)
17
+ * `A` → uppercase letter (A-Z)
18
+ * `?` → makes the rest of the mask optional
19
+ * Any other character → literal (inserted automatically)
20
+ */
21
+ function applyMask(element: HTMLInputElement, mask: string): void {
22
+ // Find the position where `?` appears (optional section starts)
23
+ const optionalIdx = mask.indexOf('?');
24
+ const cleanMask = mask.replace('?', '');
25
+
26
+ function formatValue(raw: string): string {
27
+ let result = '';
28
+ let rawIdx = 0;
29
+
30
+ for (let maskIdx = 0; maskIdx < cleanMask.length && rawIdx < raw.length; maskIdx++) {
31
+ const maskChar = cleanMask[maskIdx]!;
32
+
33
+ if (maskChar === '9') {
34
+ // Expect digit
35
+ while (rawIdx < raw.length && !/\d/.test(raw[rawIdx]!)) rawIdx++;
36
+ if (rawIdx < raw.length) {
37
+ result += raw[rawIdx]!;
38
+ rawIdx++;
39
+ } else {
40
+ break;
41
+ }
42
+ } else if (maskChar === 'A') {
43
+ // Expect uppercase letter
44
+ while (rawIdx < raw.length && !/[A-Z]/i.test(raw[rawIdx]!)) rawIdx++;
45
+ if (rawIdx < raw.length) {
46
+ result += raw[rawIdx]!.toUpperCase();
47
+ rawIdx++;
48
+ } else {
49
+ break;
50
+ }
51
+ } else {
52
+ // Literal — insert it
53
+ result += maskChar;
54
+ // If the raw char matches the literal, skip it
55
+ if (raw[rawIdx] === maskChar) rawIdx++;
56
+ }
57
+ }
58
+
59
+ return result;
60
+ }
61
+
62
+ function getPlaceholder(): string {
63
+ const required = optionalIdx >= 0 ? cleanMask.substring(0, optionalIdx) : cleanMask;
64
+ return required
65
+ .replace(/9/g, '_')
66
+ .replace(/A/g, '_');
67
+ }
68
+
69
+ // Set placeholder to show expected format
70
+ if (!element.placeholder) {
71
+ element.placeholder = getPlaceholder();
72
+ }
73
+
74
+ element.addEventListener('input', () => {
75
+ const pos = element.selectionStart ?? 0;
76
+ const oldLen = element.value.length;
77
+ element.value = formatValue(element.value);
78
+ const newLen = element.value.length;
79
+ // Preserve cursor position intelligently
80
+ const newPos = pos + (newLen - oldLen);
81
+ element.setSelectionRange(newPos, newPos);
82
+ });
83
+
84
+ element.addEventListener('blur', () => {
85
+ element.value = formatValue(element.value);
86
+ });
87
+
88
+ // Format the initial value if present
89
+ if (element.value) {
90
+ element.value = formatValue(element.value);
91
+ }
92
+ }
93
+
94
+ // -- Built-in formatter definitions -----------------------------------------
95
+
96
+ /** Taiwan ID number: A123456789 */
97
+ export const idNumberFormatter: Formatter = {
98
+ key: 'idNumber',
99
+ format: (el) => applyMask(el, 'A999999999'),
100
+ };
101
+
102
+ /** Date: YYYY-MM-DD (day is optional via `?`) */
103
+ export const dateFormatter: Formatter = {
104
+ key: 'date',
105
+ format: (el) => applyMask(el, '9999-99?-99'),
106
+ };
107
+
108
+ /** Time: HH:MM */
109
+ export const timeFormatter: Formatter = {
110
+ key: 'time',
111
+ format: (el) => applyMask(el, '99:99'),
112
+ };
113
+
114
+ export const builtInFormatters: Formatter[] = [
115
+ idNumberFormatter,
116
+ dateFormatter,
117
+ timeFormatter,
118
+ ];
@@ -0,0 +1,53 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Formatter Registry — register and apply input formatters by key
3
+ //
4
+ // Design patterns used:
5
+ // - Registry Pattern : formatters stored by key, resolved at runtime
6
+ // - Observer Pattern : integrates with VIEW.addBeforeLoad for auto-init
7
+ // - Open/Closed : add new formatters without modifying existing code
8
+ // ---------------------------------------------------------------------------
9
+
10
+ import type { Formatter } from '../types.js';
11
+
12
+ /**
13
+ * Manages input formatters keyed by name.
14
+ *
15
+ * Elements declare their desired format via the `format` HTML attribute.
16
+ * The registry matches elements to their formatter at initialization time.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * registry.add({ key: 'phone', format: (el) => applyPhoneMask(el) });
21
+ *
22
+ * // In HTML:
23
+ * // <input type="text" format="phone" />
24
+ * ```
25
+ */
26
+ export class FormatterRegistry {
27
+ private formatters = new Map<string, Formatter>();
28
+
29
+ /**
30
+ * Register a formatter. If a formatter with the same key exists, it is replaced.
31
+ */
32
+ add(formatter: Formatter): void {
33
+ this.formatters.set(formatter.key, formatter);
34
+ }
35
+
36
+ /**
37
+ * Apply registered formatters to all elements with a `format` attribute
38
+ * within the given context.
39
+ *
40
+ * This is the **beforeLoad hook** — registered with `VIEW.addBeforeLoad`
41
+ * so dynamically loaded content also gets formatted.
42
+ */
43
+ applyAll(context: HTMLElement): void {
44
+ const elements = context.querySelectorAll<HTMLInputElement>('[format]');
45
+
46
+ for (const el of elements) {
47
+ const key = el.getAttribute('format');
48
+ if (!key) continue;
49
+ const formatter = this.formatters.get(key);
50
+ formatter?.format(el);
51
+ }
52
+ }
53
+ }
package/src/index.ts ADDED
@@ -0,0 +1,142 @@
1
+ // ---------------------------------------------------------------------------
2
+ // ts-util-core — barrel export
3
+ //
4
+ // This is the main entry point. It wires all modules together and exposes
5
+ // the unified `#` (TS-Util) namespace.
6
+ //
7
+ // Usage with ES modules (recommended):
8
+ //
9
+ // import { AJAX, VIEW, MSG, Validation, Formatter } from 'ts-util-core';
10
+ //
11
+ // // or as a single namespace:
12
+ // import * as RS from 'ts-util-core';
13
+ // RS.AJAX.request({ url: '/api/save', form: myForm });
14
+ //
15
+ // Usage as a global (for non-module scripts):
16
+ //
17
+ // The library auto-registers `window.#` (accessed via `window['#']`)
18
+ // when loaded in a <script> tag without `type="module"`.
19
+ //
20
+ // Design patterns used:
21
+ // - Facade Pattern : one import gives access to all features
22
+ // - Mediator Pattern : the EventEmitter is the central coordinator —
23
+ // modules don't reference each other directly,
24
+ // they communicate through events
25
+ // ---------------------------------------------------------------------------
26
+
27
+ // Re-export types for consumers
28
+ export type {
29
+ ConstraintType,
30
+ FormDataRecord,
31
+ AjaxRequestParams,
32
+ AjaxJsonRequestParams,
33
+ ViewLoadParams,
34
+ MessageOptions,
35
+ Formatter as FormatterDefinition,
36
+ AppEventMap,
37
+ ValidationResult,
38
+ TextareaValidationResult,
39
+ } from './types.js';
40
+
41
+ export type { ConstraintHandler } from './validation/constraints.js';
42
+ export type { BeforeLoadHook } from './core/view.js';
43
+
44
+ // Re-export classes for advanced use
45
+ export { EventEmitter } from './core/event-emitter.js';
46
+ export { Ajax } from './core/ajax.js';
47
+ export { View } from './core/view.js';
48
+ export { Message } from './core/message.js';
49
+ export { Validator } from './validation/validator.js';
50
+ export { FormatterRegistry } from './formatting/registry.js';
51
+
52
+ // Re-export utilities
53
+ export { sprintf } from './utils/sprintf.js';
54
+ export { formToJSON, isDateValid, parseHTML, scrollToElement, defaults } from './utils/dom.js';
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Pre-wired singleton instances — the `#` namespace
58
+ // ---------------------------------------------------------------------------
59
+
60
+ import { EventEmitter } from './core/event-emitter.js';
61
+ import { Ajax } from './core/ajax.js';
62
+ import { View } from './core/view.js';
63
+ import { Message } from './core/message.js';
64
+ import { Validator } from './validation/validator.js';
65
+ import { FormatterRegistry } from './formatting/registry.js';
66
+ import { builtInFormatters } from './formatting/formatters.js';
67
+ import type { AppEventMap } from './types.js';
68
+
69
+ // 1. Create the shared event bus (Mediator)
70
+ const emitter = new EventEmitter<AppEventMap>();
71
+
72
+ // 2. Instantiate modules — they receive the emitter, not each other
73
+ const ajax = new Ajax(emitter);
74
+ const view = new View(emitter, ajax);
75
+ const msg = new Message();
76
+ const validator = new Validator(emitter);
77
+ const formatter = new FormatterRegistry();
78
+
79
+ // 3. Wire cross-module dependencies via hooks (Observer Pattern)
80
+ // — The Validator tells the AJAX module how to validate forms
81
+ ajax.setValidator((form) => validator.validate(form));
82
+
83
+ // — The Validator registers its beforeLoad hook with VIEW
84
+ view.addBeforeLoad((context) => validator.initConstraints(context));
85
+
86
+ // — The FormatterRegistry registers its beforeLoad hook with VIEW
87
+ view.addBeforeLoad((context) => formatter.applyAll(context));
88
+
89
+ // 4. Register built-in formatters
90
+ for (const f of builtInFormatters) {
91
+ formatter.add(f);
92
+ }
93
+
94
+ // 5. Export pre-wired instances as the public API
95
+ /** AJAX module — HTTP requests with lifecycle hooks. */
96
+ export const AJAX = ajax;
97
+
98
+ /** VIEW module — load and initialize HTML fragments. */
99
+ export const VIEW = view;
100
+
101
+ /** MSG module — dialogs and notifications. */
102
+ export const MSG = msg;
103
+
104
+ /** Validation module — form validation engine. */
105
+ export const Validation = validator;
106
+
107
+ /** Formatter module — input masking registry. */
108
+ export const Formatter = formatter;
109
+
110
+ /** Event bus — subscribe to library-wide lifecycle events. */
111
+ export const Events = emitter;
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Auto-initialize on DOMContentLoaded (when running in a browser)
115
+ // ---------------------------------------------------------------------------
116
+
117
+ if (typeof document !== 'undefined') {
118
+ const init = () => view.initPage();
119
+
120
+ if (document.readyState === 'loading') {
121
+ document.addEventListener('DOMContentLoaded', init);
122
+ } else {
123
+ init();
124
+ }
125
+ }
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // Global registration for non-module environments: window['#']
129
+ // ---------------------------------------------------------------------------
130
+
131
+ if (typeof window !== 'undefined') {
132
+ const ns = {
133
+ AJAX: ajax,
134
+ VIEW: view,
135
+ MSG: msg,
136
+ Validation: validator,
137
+ Formatter: formatter,
138
+ Events: emitter,
139
+ };
140
+
141
+ (window as unknown as Record<string, unknown>)['#'] = ns;
142
+ }