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.
- package/README.md +411 -0
- package/dist/core/ajax.d.ts +30 -0
- package/dist/core/ajax.d.ts.map +1 -0
- package/dist/core/ajax.js +110 -0
- package/dist/core/ajax.js.map +1 -0
- package/dist/core/event-emitter.d.ts +29 -0
- package/dist/core/event-emitter.d.ts.map +1 -0
- package/dist/core/event-emitter.js +67 -0
- package/dist/core/event-emitter.js.map +1 -0
- package/dist/core/message.d.ts +28 -0
- package/dist/core/message.d.ts.map +1 -0
- package/dist/core/message.js +172 -0
- package/dist/core/message.js.map +1 -0
- package/dist/core/view.d.ts +49 -0
- package/dist/core/view.d.ts.map +1 -0
- package/dist/core/view.js +87 -0
- package/dist/core/view.js.map +1 -0
- package/dist/formatting/formatters.d.ts +9 -0
- package/dist/formatting/formatters.d.ts.map +1 -0
- package/dist/formatting/formatters.js +109 -0
- package/dist/formatting/formatters.js.map +1 -0
- package/dist/formatting/registry.d.ts +31 -0
- package/dist/formatting/registry.d.ts.map +1 -0
- package/dist/formatting/registry.js +49 -0
- package/dist/formatting/registry.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +104 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +66 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/dom.d.ts +38 -0
- package/dist/utils/dom.d.ts.map +1 -0
- package/dist/utils/dom.js +95 -0
- package/dist/utils/dom.js.map +1 -0
- package/dist/utils/sprintf.d.ts +16 -0
- package/dist/utils/sprintf.d.ts.map +1 -0
- package/dist/utils/sprintf.js +116 -0
- package/dist/utils/sprintf.js.map +1 -0
- package/dist/validation/constraints.d.ts +23 -0
- package/dist/validation/constraints.d.ts.map +1 -0
- package/dist/validation/constraints.js +131 -0
- package/dist/validation/constraints.js.map +1 -0
- package/dist/validation/validator.d.ts +45 -0
- package/dist/validation/validator.d.ts.map +1 -0
- package/dist/validation/validator.js +210 -0
- package/dist/validation/validator.js.map +1 -0
- package/package.json +26 -0
- package/readme.txt +4 -0
- package/src/core/ajax.ts +127 -0
- package/src/core/event-emitter.ts +84 -0
- package/src/core/message.ts +212 -0
- package/src/core/view.ts +101 -0
- package/src/formatting/formatters.ts +118 -0
- package/src/formatting/registry.ts +53 -0
- package/src/index.ts +142 -0
- package/src/types.ts +85 -0
- package/src/utils/dom.ts +105 -0
- package/src/utils/sprintf.ts +141 -0
- package/src/validation/constraints.ts +168 -0
- package/src/validation/validator.ts +276 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { EventEmitter } from '../core/event-emitter.js';
|
|
2
|
+
import type { AppEventMap } from '../types.js';
|
|
3
|
+
import { type ConstraintHandler } from './constraints.js';
|
|
4
|
+
type InvalidCallback = (labelNames: string[], elements: HTMLElement[]) => void;
|
|
5
|
+
type TextareaTooLongCallback = (labelNames: string[], maxlengths: number[], elements: HTMLElement[]) => void;
|
|
6
|
+
export declare class Validator {
|
|
7
|
+
private emitter;
|
|
8
|
+
private constraints;
|
|
9
|
+
private requiredInvalidCb;
|
|
10
|
+
private textareaTooLongCb;
|
|
11
|
+
constructor(emitter: EventEmitter<AppEventMap>);
|
|
12
|
+
/**
|
|
13
|
+
* Register a custom constraint handler.
|
|
14
|
+
*
|
|
15
|
+
* Registry Pattern — extend validation without modifying library code.
|
|
16
|
+
*/
|
|
17
|
+
addConstraint(handler: ConstraintHandler): void;
|
|
18
|
+
/**
|
|
19
|
+
* Override the default behavior when required-field validation fails.
|
|
20
|
+
*
|
|
21
|
+
* Strategy Pattern — consumers inject their own notification UI.
|
|
22
|
+
*/
|
|
23
|
+
setRequiredInvalidCallback(cb: InvalidCallback): void;
|
|
24
|
+
setTextareaTooLongCallback(cb: TextareaTooLongCallback): void;
|
|
25
|
+
/**
|
|
26
|
+
* Attach constraint handlers to all matching elements within `context`.
|
|
27
|
+
*
|
|
28
|
+
* This is the **beforeLoad hook** — it is registered with `VIEW.addBeforeLoad`
|
|
29
|
+
* so that dynamically loaded content is also initialized.
|
|
30
|
+
*/
|
|
31
|
+
initConstraints(context: HTMLElement): void;
|
|
32
|
+
private bindIgnoreValidation;
|
|
33
|
+
/**
|
|
34
|
+
* Validate all required fields and textarea lengths within a container.
|
|
35
|
+
*
|
|
36
|
+
* @returns `true` if all validations pass.
|
|
37
|
+
*/
|
|
38
|
+
validate(container: HTMLElement): boolean;
|
|
39
|
+
private checkTextareaLengths;
|
|
40
|
+
private checkRequired;
|
|
41
|
+
private handleRequiredInvalid;
|
|
42
|
+
private handleTextareaTooLong;
|
|
43
|
+
}
|
|
44
|
+
export {};
|
|
45
|
+
//# sourceMappingURL=validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../src/validation/validator.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,KAAK,EAAE,WAAW,EAA8C,MAAM,aAAa,CAAC;AAE3F,OAAO,EACL,KAAK,iBAAiB,EAEvB,MAAM,kBAAkB,CAAC;AAE1B,KAAK,eAAe,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,WAAW,EAAE,KAAK,IAAI,CAAC;AAC/E,KAAK,uBAAuB,GAAG,CAC7B,UAAU,EAAE,MAAM,EAAE,EACpB,UAAU,EAAE,MAAM,EAAE,EACpB,QAAQ,EAAE,WAAW,EAAE,KACpB,IAAI,CAAC;AAEV,qBAAa,SAAS;IACpB,OAAO,CAAC,OAAO,CAA4B;IAC3C,OAAO,CAAC,WAAW,CAA6C;IAChE,OAAO,CAAC,iBAAiB,CAAgC;IACzD,OAAO,CAAC,iBAAiB,CAAwC;gBAErD,OAAO,EAAE,YAAY,CAAC,WAAW,CAAC;IAW9C;;;;OAIG;IACH,aAAa,CAAC,OAAO,EAAE,iBAAiB,GAAG,IAAI;IAM/C;;;;OAIG;IACH,0BAA0B,CAAC,EAAE,EAAE,eAAe,GAAG,IAAI;IAIrD,0BAA0B,CAAC,EAAE,EAAE,uBAAuB,GAAG,IAAI;IAM7D;;;;;OAKG;IACH,eAAe,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI;IA+B3C,OAAO,CAAC,oBAAoB;IAyB5B;;;;OAIG;IACH,QAAQ,CAAC,SAAS,EAAE,WAAW,GAAG,OAAO;IAkBzC,OAAO,CAAC,oBAAoB;IAuB5B,OAAO,CAAC,aAAa;IA2DrB,OAAO,CAAC,qBAAqB;IAiB7B,OAAO,CAAC,qBAAqB;CAuB9B"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Validator — form validation engine
|
|
3
|
+
//
|
|
4
|
+
// Design patterns used:
|
|
5
|
+
// - Observer Pattern : integrates with VIEW.addBeforeLoad to auto-init
|
|
6
|
+
// - Strategy Pattern : invalid-field handling is injectable via the emitter
|
|
7
|
+
// - Decorator Pattern : constraints are attached to elements by attribute
|
|
8
|
+
// - Registry Pattern : ConstraintHandler lookup by name
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
import { scrollToElement } from '../utils/dom.js';
|
|
11
|
+
import { builtInConstraints, } from './constraints.js';
|
|
12
|
+
export class Validator {
|
|
13
|
+
emitter;
|
|
14
|
+
constraints = new Map();
|
|
15
|
+
requiredInvalidCb = null;
|
|
16
|
+
textareaTooLongCb = null;
|
|
17
|
+
constructor(emitter) {
|
|
18
|
+
this.emitter = emitter;
|
|
19
|
+
// Register built-in constraints
|
|
20
|
+
for (const c of builtInConstraints) {
|
|
21
|
+
this.constraints.set(c.name, c);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// -- Constraint Registration ------------------------------------------------
|
|
25
|
+
/**
|
|
26
|
+
* Register a custom constraint handler.
|
|
27
|
+
*
|
|
28
|
+
* Registry Pattern — extend validation without modifying library code.
|
|
29
|
+
*/
|
|
30
|
+
addConstraint(handler) {
|
|
31
|
+
this.constraints.set(handler.name, handler);
|
|
32
|
+
}
|
|
33
|
+
// -- Callback Setters (Strategy Pattern) ------------------------------------
|
|
34
|
+
/**
|
|
35
|
+
* Override the default behavior when required-field validation fails.
|
|
36
|
+
*
|
|
37
|
+
* Strategy Pattern — consumers inject their own notification UI.
|
|
38
|
+
*/
|
|
39
|
+
setRequiredInvalidCallback(cb) {
|
|
40
|
+
this.requiredInvalidCb = cb;
|
|
41
|
+
}
|
|
42
|
+
setTextareaTooLongCallback(cb) {
|
|
43
|
+
this.textareaTooLongCb = cb;
|
|
44
|
+
}
|
|
45
|
+
// -- Initialization (called by VIEW.addBeforeLoad) --------------------------
|
|
46
|
+
/**
|
|
47
|
+
* Attach constraint handlers to all matching elements within `context`.
|
|
48
|
+
*
|
|
49
|
+
* This is the **beforeLoad hook** — it is registered with `VIEW.addBeforeLoad`
|
|
50
|
+
* so that dynamically loaded content is also initialized.
|
|
51
|
+
*/
|
|
52
|
+
initConstraints(context) {
|
|
53
|
+
const elements = context.querySelectorAll('[constraint]');
|
|
54
|
+
for (const el of elements) {
|
|
55
|
+
const tokens = (el.getAttribute('constraint') || '').split(/\s+/);
|
|
56
|
+
for (const token of tokens) {
|
|
57
|
+
if (!token)
|
|
58
|
+
continue;
|
|
59
|
+
const handler = this.constraints.get(token);
|
|
60
|
+
handler?.attach(el);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Auto-select text on focus (text inputs and textareas)
|
|
64
|
+
const focusTargets = context.querySelectorAll('input[type="text"], textarea');
|
|
65
|
+
for (const el of focusTargets) {
|
|
66
|
+
el.addEventListener('focus', () => {
|
|
67
|
+
setTimeout(() => el.select(), 1);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
// Ignore-validation submit buttons
|
|
71
|
+
this.bindIgnoreValidation(context);
|
|
72
|
+
}
|
|
73
|
+
// -- Form Submit Integration ------------------------------------------------
|
|
74
|
+
bindIgnoreValidation(context) {
|
|
75
|
+
const buttons = context.querySelectorAll("input[type='submit'][ignoreValidation='true']");
|
|
76
|
+
for (const btn of buttons) {
|
|
77
|
+
btn.addEventListener('click', () => {
|
|
78
|
+
btn.closest('form')?.classList.add('ignoreValidation');
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const forms = context.querySelectorAll('form');
|
|
82
|
+
for (const form of forms) {
|
|
83
|
+
form.addEventListener('submit', (e) => {
|
|
84
|
+
if (!form.classList.contains('ignoreValidation')) {
|
|
85
|
+
if (!this.validate(form)) {
|
|
86
|
+
e.preventDefault();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
form.classList.remove('ignoreValidation');
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// -- Core Validation Logic --------------------------------------------------
|
|
94
|
+
/**
|
|
95
|
+
* Validate all required fields and textarea lengths within a container.
|
|
96
|
+
*
|
|
97
|
+
* @returns `true` if all validations pass.
|
|
98
|
+
*/
|
|
99
|
+
validate(container) {
|
|
100
|
+
// Check textarea max-length first
|
|
101
|
+
const tooLong = this.checkTextareaLengths(container);
|
|
102
|
+
if (!tooLong.valid) {
|
|
103
|
+
this.handleTextareaTooLong(tooLong);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
// Check required fields
|
|
107
|
+
const result = this.checkRequired(container);
|
|
108
|
+
if (!result.valid) {
|
|
109
|
+
this.handleRequiredInvalid(result);
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
checkTextareaLengths(container) {
|
|
115
|
+
const textareas = container.querySelectorAll('textarea[maxlength]');
|
|
116
|
+
const invalidElements = [];
|
|
117
|
+
const labelNames = [];
|
|
118
|
+
const maxlengths = [];
|
|
119
|
+
for (const ta of textareas) {
|
|
120
|
+
const max = parseInt(ta.getAttribute('maxlength') || '0', 10);
|
|
121
|
+
if (ta.value.length > max) {
|
|
122
|
+
invalidElements.push(ta);
|
|
123
|
+
labelNames.push(ta.getAttribute('labelName') || ta.name || '');
|
|
124
|
+
maxlengths.push(max);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
valid: invalidElements.length === 0,
|
|
129
|
+
invalidElements,
|
|
130
|
+
labelNames,
|
|
131
|
+
maxlengths,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
checkRequired(container) {
|
|
135
|
+
const elements = container.querySelectorAll("[constraint~='required']");
|
|
136
|
+
// Group by name (for radio/checkbox groups)
|
|
137
|
+
const groups = new Map();
|
|
138
|
+
let idCounter = 0;
|
|
139
|
+
for (const el of elements) {
|
|
140
|
+
const input = el;
|
|
141
|
+
let name = input.name || `__unnamed_${idCounter++}`;
|
|
142
|
+
if (!groups.has(name)) {
|
|
143
|
+
groups.set(name, []);
|
|
144
|
+
}
|
|
145
|
+
groups.get(name).push(el);
|
|
146
|
+
}
|
|
147
|
+
const invalidElements = [];
|
|
148
|
+
const labelNames = [];
|
|
149
|
+
for (const [, groupElements] of groups) {
|
|
150
|
+
let notEmpty = false;
|
|
151
|
+
for (const el of groupElements) {
|
|
152
|
+
const input = el;
|
|
153
|
+
if (input.type === 'radio' || input.type === 'checkbox') {
|
|
154
|
+
if (input.checked)
|
|
155
|
+
notEmpty = true;
|
|
156
|
+
}
|
|
157
|
+
else if ((el.getAttribute('constraint') || '').includes('number')) {
|
|
158
|
+
if (input.value !== '0' && input.value !== '')
|
|
159
|
+
notEmpty = true;
|
|
160
|
+
}
|
|
161
|
+
else if (input.value && input.value !== '') {
|
|
162
|
+
notEmpty = true;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (!notEmpty) {
|
|
166
|
+
const first = groupElements[0];
|
|
167
|
+
invalidElements.push(first);
|
|
168
|
+
labelNames.push(first.getAttribute('labelName') ||
|
|
169
|
+
first.name ||
|
|
170
|
+
'');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
valid: invalidElements.length === 0,
|
|
175
|
+
invalidElements,
|
|
176
|
+
labelNames,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
// -- Default Failure Handlers -----------------------------------------------
|
|
180
|
+
handleRequiredInvalid(result) {
|
|
181
|
+
this.emitter.emit('validation:invalid', {
|
|
182
|
+
labelNames: result.labelNames,
|
|
183
|
+
elements: result.invalidElements,
|
|
184
|
+
});
|
|
185
|
+
if (this.requiredInvalidCb) {
|
|
186
|
+
this.requiredInvalidCb(result.labelNames, result.invalidElements);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
// Default behavior: alert + scroll
|
|
190
|
+
const text = result.labelNames.map((n) => `"${n}"`).join(', ');
|
|
191
|
+
scrollToElement(result.invalidElements[0]);
|
|
192
|
+
window.alert(`${text} is required!`);
|
|
193
|
+
}
|
|
194
|
+
handleTextareaTooLong(result) {
|
|
195
|
+
this.emitter.emit('validation:textareaTooLong', {
|
|
196
|
+
labelNames: result.labelNames,
|
|
197
|
+
maxlengths: result.maxlengths,
|
|
198
|
+
elements: result.invalidElements,
|
|
199
|
+
});
|
|
200
|
+
if (this.textareaTooLongCb) {
|
|
201
|
+
this.textareaTooLongCb(result.labelNames, result.maxlengths, result.invalidElements);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
// Default behavior: alert + scroll
|
|
205
|
+
const lines = result.labelNames.map((name, i) => `"${name}" length is too long, max length is ${result.maxlengths[i]}!`);
|
|
206
|
+
scrollToElement(result.invalidElements[0]);
|
|
207
|
+
window.alert(lines.join('\n'));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=validator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validator.js","sourceRoot":"","sources":["../../src/validation/validator.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ts-util-core",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "TS-Util Core Library — form validation, AJAX, view loading, messaging, and formatting utilities",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"src"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"dev": "tsc --watch"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"typescript": "^5.7.0"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT"
|
|
26
|
+
}
|
package/readme.txt
ADDED
package/src/core/ajax.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// AJAX module — fetch-based HTTP client with lifecycle hooks
|
|
3
|
+
//
|
|
4
|
+
// Design patterns used:
|
|
5
|
+
// - Facade Pattern : one `request()` call orchestrates validate →
|
|
6
|
+
// block → serialize → fetch → unblock
|
|
7
|
+
// - Template Method Pattern: `requestJSON` / `post` / `postJSON` delegate
|
|
8
|
+
// to the base `request` with tweaked options
|
|
9
|
+
// - Strategy Pattern : loading overlay behavior is injected via
|
|
10
|
+
// the EventEmitter (ajax:before / ajax:after)
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
import type { EventEmitter } from './event-emitter.js';
|
|
14
|
+
import type { AppEventMap, AjaxRequestParams, AjaxJsonRequestParams } from '../types.js';
|
|
15
|
+
import { formToJSON } from '../utils/dom.js';
|
|
16
|
+
|
|
17
|
+
export class Ajax {
|
|
18
|
+
private emitter: EventEmitter<AppEventMap>;
|
|
19
|
+
private validateForm: ((form: HTMLElement) => boolean) | null = null;
|
|
20
|
+
|
|
21
|
+
constructor(emitter: EventEmitter<AppEventMap>) {
|
|
22
|
+
this.emitter = emitter;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Register the form validation function (injected by the Validation module). */
|
|
26
|
+
setValidator(fn: (form: HTMLElement) => boolean): void {
|
|
27
|
+
this.validateForm = fn;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Send an HTTP request (POST by default).
|
|
32
|
+
*
|
|
33
|
+
* Lifecycle:
|
|
34
|
+
* 1. Validate form (if `params.form` provided)
|
|
35
|
+
* 2. Emit `ajax:before`
|
|
36
|
+
* 3. Serialize form data + merge `params.data`
|
|
37
|
+
* 4. `fetch()`
|
|
38
|
+
* 5. Emit `ajax:after` (on success) or `ajax:error` (on failure)
|
|
39
|
+
* 6. Call `params.success` / `params.error` / `params.complete`
|
|
40
|
+
*
|
|
41
|
+
* @returns The raw `Response`, or `undefined` if validation failed.
|
|
42
|
+
*/
|
|
43
|
+
async request(params: AjaxRequestParams): Promise<Response | undefined> {
|
|
44
|
+
// 1. Validate
|
|
45
|
+
if (params.form && this.validateForm) {
|
|
46
|
+
if (!this.validateForm(params.form)) return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 2. Before-hook
|
|
50
|
+
if (!params.noblock) {
|
|
51
|
+
this.emitter.emit('ajax:before', { url: params.url });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 3. Build body
|
|
55
|
+
const body: Record<string, unknown> = {};
|
|
56
|
+
|
|
57
|
+
if (params.form) {
|
|
58
|
+
const formData = formToJSON(params.form, {
|
|
59
|
+
ignoreDisabled: params.ignoreDisabled,
|
|
60
|
+
});
|
|
61
|
+
Object.assign(body, formData);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (params.data) {
|
|
65
|
+
Object.assign(body, params.data);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 4. Fetch
|
|
69
|
+
const headers: Record<string, string> = {
|
|
70
|
+
'Content-Type': 'application/json',
|
|
71
|
+
'Ajax-Call': 'true',
|
|
72
|
+
...params.headers,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const response = await fetch(params.url, {
|
|
77
|
+
method: 'POST',
|
|
78
|
+
headers,
|
|
79
|
+
body: JSON.stringify(body),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 5a. After-hook (success)
|
|
87
|
+
if (!params.noblock) {
|
|
88
|
+
this.emitter.emit('ajax:after', { url: params.url });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
params.success?.(response);
|
|
92
|
+
return response;
|
|
93
|
+
} catch (err) {
|
|
94
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
95
|
+
|
|
96
|
+
// 5b. After-hook (error)
|
|
97
|
+
if (!params.noblock) {
|
|
98
|
+
this.emitter.emit('ajax:after', { url: params.url });
|
|
99
|
+
}
|
|
100
|
+
this.emitter.emit('ajax:error', { url: params.url, error });
|
|
101
|
+
|
|
102
|
+
params.error?.(error);
|
|
103
|
+
return undefined;
|
|
104
|
+
} finally {
|
|
105
|
+
params.complete?.();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Send a request and parse the response as JSON.
|
|
111
|
+
*
|
|
112
|
+
* Template Method — delegates to `request()`, adds JSON parsing.
|
|
113
|
+
*/
|
|
114
|
+
async requestJSON<T = unknown>(
|
|
115
|
+
params: AjaxJsonRequestParams<T>,
|
|
116
|
+
): Promise<T | undefined> {
|
|
117
|
+
const response = await this.request({
|
|
118
|
+
...params,
|
|
119
|
+
success: undefined, // we handle success after JSON parse
|
|
120
|
+
});
|
|
121
|
+
if (!response) return undefined;
|
|
122
|
+
|
|
123
|
+
const data = (await response.json()) as T;
|
|
124
|
+
params.success?.(data);
|
|
125
|
+
return data;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Typed EventEmitter — the unified hook / callback system
|
|
3
|
+
//
|
|
4
|
+
// Design patterns used:
|
|
5
|
+
// - Observer Pattern : multiple listeners subscribe to named events
|
|
6
|
+
// - Strategy Pattern : consumers replace default behavior via listeners
|
|
7
|
+
// - Generics : the event map is fully typed at compile time
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A fully typed event emitter.
|
|
12
|
+
*
|
|
13
|
+
* `TEvents` is a map of `{ eventName: payloadType }`.
|
|
14
|
+
* All `on`, `off`, and `emit` calls are type-checked against this map.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* const emitter = new EventEmitter<{ 'ajax:before': { url: string } }>();
|
|
19
|
+
* emitter.on('ajax:before', ({ url }) => console.log(url));
|
|
20
|
+
* emitter.emit('ajax:before', { url: '/api' });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export class EventEmitter<TEvents extends { [K in keyof TEvents]: unknown }> {
|
|
24
|
+
private listeners = new Map<
|
|
25
|
+
keyof TEvents,
|
|
26
|
+
Set<(payload: never) => void>
|
|
27
|
+
>();
|
|
28
|
+
|
|
29
|
+
/** Subscribe to an event. Returns an unsubscribe function. */
|
|
30
|
+
on<K extends keyof TEvents>(
|
|
31
|
+
event: K,
|
|
32
|
+
listener: (payload: TEvents[K]) => void,
|
|
33
|
+
): () => void {
|
|
34
|
+
if (!this.listeners.has(event)) {
|
|
35
|
+
this.listeners.set(event, new Set());
|
|
36
|
+
}
|
|
37
|
+
const set = this.listeners.get(event)!;
|
|
38
|
+
set.add(listener as (payload: never) => void);
|
|
39
|
+
|
|
40
|
+
// Return unsubscribe function for convenience
|
|
41
|
+
return () => {
|
|
42
|
+
set.delete(listener as (payload: never) => void);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Unsubscribe a specific listener. */
|
|
47
|
+
off<K extends keyof TEvents>(
|
|
48
|
+
event: K,
|
|
49
|
+
listener: (payload: TEvents[K]) => void,
|
|
50
|
+
): void {
|
|
51
|
+
this.listeners.get(event)?.delete(listener as (payload: never) => void);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Subscribe to an event — listener is automatically removed after one call. */
|
|
55
|
+
once<K extends keyof TEvents>(
|
|
56
|
+
event: K,
|
|
57
|
+
listener: (payload: TEvents[K]) => void,
|
|
58
|
+
): () => void {
|
|
59
|
+
const wrapper = ((payload: TEvents[K]) => {
|
|
60
|
+
this.off(event, wrapper);
|
|
61
|
+
listener(payload);
|
|
62
|
+
}) as (payload: TEvents[K]) => void;
|
|
63
|
+
|
|
64
|
+
return this.on(event, wrapper);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Emit an event, calling all registered listeners with the payload. */
|
|
68
|
+
emit<K extends keyof TEvents>(event: K, payload: TEvents[K]): void {
|
|
69
|
+
const set = this.listeners.get(event);
|
|
70
|
+
if (!set) return;
|
|
71
|
+
for (const listener of set) {
|
|
72
|
+
(listener as (payload: TEvents[K]) => void)(payload);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Remove all listeners for a specific event, or all events if none specified. */
|
|
77
|
+
clear(event?: keyof TEvents): void {
|
|
78
|
+
if (event) {
|
|
79
|
+
this.listeners.delete(event);
|
|
80
|
+
} else {
|
|
81
|
+
this.listeners.clear();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|