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
package/src/types.ts ADDED
@@ -0,0 +1,85 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Shared type definitions for ts-util-core
3
+ // ---------------------------------------------------------------------------
4
+
5
+ /** Supported constraint types declared via the `constraint` HTML attribute. */
6
+ export type ConstraintType =
7
+ | 'required'
8
+ | 'date'
9
+ | 'number'
10
+ | 'time'
11
+ | 'upperCase'
12
+ | 'onlyEn';
13
+
14
+ /** A record whose values are either a single string or an array of strings. */
15
+ export type FormDataRecord = Record<string, string | string[]>;
16
+
17
+ // -- AJAX -------------------------------------------------------------------
18
+
19
+ export interface AjaxRequestParams {
20
+ url: string;
21
+ data?: Record<string, unknown>;
22
+ form?: HTMLElement;
23
+ ignoreDisabled?: boolean;
24
+ noblock?: boolean;
25
+ headers?: Record<string, string>;
26
+ success?: (data: unknown) => void;
27
+ error?: (error: Error) => void;
28
+ complete?: () => void;
29
+ }
30
+
31
+ export interface AjaxJsonRequestParams<T = unknown> extends Omit<AjaxRequestParams, 'success'> {
32
+ success?: (data: T) => void;
33
+ }
34
+
35
+ // -- VIEW -------------------------------------------------------------------
36
+
37
+ export interface ViewLoadParams extends Omit<AjaxRequestParams, 'success'> {
38
+ success?: (content: HTMLElement) => void;
39
+ }
40
+
41
+ // -- MSG --------------------------------------------------------------------
42
+
43
+ export interface MessageOptions {
44
+ autoclose?: number;
45
+ title?: string;
46
+ }
47
+
48
+ export interface ConfirmButton {
49
+ label: string;
50
+ value: string;
51
+ }
52
+
53
+ // -- Validation -------------------------------------------------------------
54
+
55
+ export interface ValidationResult {
56
+ valid: boolean;
57
+ invalidElements: HTMLElement[];
58
+ labelNames: string[];
59
+ }
60
+
61
+ export interface TextareaValidationResult extends ValidationResult {
62
+ maxlengths: number[];
63
+ }
64
+
65
+ // -- Formatting -------------------------------------------------------------
66
+
67
+ export interface Formatter {
68
+ key: string;
69
+ format: (element: HTMLInputElement) => void;
70
+ }
71
+
72
+ // -- Hook / Event Map -------------------------------------------------------
73
+
74
+ export interface AppEventMap {
75
+ 'ajax:before': { url: string };
76
+ 'ajax:after': { url: string };
77
+ 'ajax:error': { url: string; error: Error };
78
+ 'view:beforeLoad': { context: HTMLElement };
79
+ 'validation:invalid': { labelNames: string[]; elements: HTMLElement[] };
80
+ 'validation:textareaTooLong': {
81
+ labelNames: string[];
82
+ maxlengths: number[];
83
+ elements: HTMLElement[];
84
+ };
85
+ }
@@ -0,0 +1,105 @@
1
+ // ---------------------------------------------------------------------------
2
+ // DOM utility functions — vanilla TypeScript replacements for jQuery helpers
3
+ //
4
+ // Design patterns used:
5
+ // - Facade Pattern : simple API over verbose native DOM methods
6
+ // ---------------------------------------------------------------------------
7
+
8
+ import type { FormDataRecord } from '../types.js';
9
+
10
+ /**
11
+ * Convert all named form elements within a container to a key-value record.
12
+ *
13
+ * Replaces the original jQuery-based `formToJSON`.
14
+ *
15
+ * @param container - The DOM element containing form inputs.
16
+ * @param options.ignoreDisabled - Skip disabled elements (default `false`).
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * const data = formToJSON(document.getElementById('myForm')!);
21
+ * // { username: "alice", roles: ["admin", "editor"] }
22
+ * ```
23
+ */
24
+ export function formToJSON(
25
+ container: HTMLElement,
26
+ options: { ignoreDisabled?: boolean } = {},
27
+ ): FormDataRecord {
28
+ const { ignoreDisabled = false } = options;
29
+
30
+ const selectors =
31
+ 'input[name], textarea[name], select[name]';
32
+ let elements = Array.from(
33
+ container.querySelectorAll<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>(selectors),
34
+ );
35
+
36
+ if (ignoreDisabled) {
37
+ elements = elements.filter((el) => !el.disabled);
38
+ }
39
+
40
+ const params: FormDataRecord = {};
41
+
42
+ for (const el of elements) {
43
+ const name = el.name;
44
+ if (!name) continue;
45
+
46
+ if (el instanceof HTMLInputElement && el.type === 'checkbox') {
47
+ if (!(name in params)) params[name] = [];
48
+ if (el.checked) (params[name] as string[]).push(el.value);
49
+ } else if (el instanceof HTMLSelectElement && el.multiple) {
50
+ if (!(name in params)) params[name] = [];
51
+ const selected = Array.from(el.selectedOptions).map((o) => o.value);
52
+ (params[name] as string[]).push(...selected);
53
+ } else if (el instanceof HTMLInputElement && el.type === 'radio') {
54
+ if (el.checked) params[name] = el.value;
55
+ } else {
56
+ const existing = params[name];
57
+ if (existing === undefined) {
58
+ params[name] = el.value;
59
+ } else if (typeof existing === 'string') {
60
+ params[name] = [existing, el.value];
61
+ } else {
62
+ existing.push(el.value);
63
+ }
64
+ }
65
+ }
66
+
67
+ return params;
68
+ }
69
+
70
+ /**
71
+ * Check whether a value is a valid date.
72
+ *
73
+ * Accepts a `Date` object or a string in `yyyy-MM-dd` / `yyyy/MM/dd` format.
74
+ */
75
+ export function isDateValid(date: Date | string): boolean {
76
+ if (date instanceof Date) return !isNaN(date.getTime());
77
+ return /^\d{4}[/-]\d{1,2}[/-]\d{1,2}$/.test(date);
78
+ }
79
+
80
+ /**
81
+ * Parse an HTML string into a `DocumentFragment`.
82
+ */
83
+ export function parseHTML(html: string): DocumentFragment {
84
+ const template = document.createElement('template');
85
+ template.innerHTML = html.trim();
86
+ return template.content;
87
+ }
88
+
89
+ /**
90
+ * Smoothly scroll an element into view.
91
+ */
92
+ export function scrollToElement(element: HTMLElement): void {
93
+ element.scrollIntoView({ behavior: 'smooth', block: 'center' });
94
+ }
95
+
96
+ /**
97
+ * Merge multiple option objects (shallow). Later sources override earlier ones.
98
+ * A typed, zero-dependency replacement for `$.extend()`.
99
+ */
100
+ export function defaults<T extends Record<string, unknown>>(
101
+ base: T,
102
+ ...overrides: Partial<T>[]
103
+ ): T {
104
+ return Object.assign({}, base, ...overrides);
105
+ }
@@ -0,0 +1,141 @@
1
+ // ---------------------------------------------------------------------------
2
+ // sprintf — printf-style string formatting
3
+ //
4
+ // Design patterns used:
5
+ // - Pure Function : no side effects, deterministic output
6
+ // ---------------------------------------------------------------------------
7
+
8
+ interface FormatMatch {
9
+ match: string;
10
+ left: boolean;
11
+ sign: string;
12
+ pad: string;
13
+ min: number;
14
+ precision: number | undefined;
15
+ code: string;
16
+ negative: boolean;
17
+ argument: string;
18
+ }
19
+
20
+ function convert(m: FormatMatch, nosign = false): string {
21
+ const sign = nosign ? '' : m.negative ? '-' : m.sign;
22
+ const padLen = m.min - m.argument.length + 1 - sign.length;
23
+ const pad = padLen > 0 ? m.pad.repeat(padLen) : '';
24
+
25
+ if (!m.left) {
26
+ return m.pad === '0' || nosign
27
+ ? sign + pad + m.argument
28
+ : pad + sign + m.argument;
29
+ }
30
+ return m.pad === '0' || nosign
31
+ ? sign + m.argument + pad.replace(/0/g, ' ')
32
+ : sign + m.argument + pad;
33
+ }
34
+
35
+ /**
36
+ * C-style `sprintf` implementation.
37
+ *
38
+ * Supported format specifiers: `%b` `%c` `%d` `%f` `%o` `%s` `%x` `%X` `%%`
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * sprintf("Hello %s, you are %d years old", "Alice", 30)
43
+ * // → "Hello Alice, you are 30 years old"
44
+ *
45
+ * sprintf("%05d", 42)
46
+ * // → "00042"
47
+ * ```
48
+ */
49
+ export function sprintf(format: string, ...args: unknown[]): string {
50
+ const exp =
51
+ /(%([%]|(-)?(\+|\x20)?(0)?(\d+)?(\.(\d)?)?([bcdfosxX])))/g;
52
+ const matches: FormatMatch[] = [];
53
+ const strings: string[] = [];
54
+ let convCount = 0;
55
+ let matchPosEnd = 0;
56
+ let result: RegExpExecArray | null;
57
+
58
+ while ((result = exp.exec(format)) !== null) {
59
+ if (result[9]) convCount++;
60
+
61
+ const stringPosStart = matchPosEnd;
62
+ const stringPosEnd = exp.lastIndex - result[0].length;
63
+ strings.push(format.substring(stringPosStart, stringPosEnd));
64
+ matchPosEnd = exp.lastIndex;
65
+
66
+ const argValue = args[convCount - 1];
67
+
68
+ matches.push({
69
+ match: result[0],
70
+ left: !!result[3],
71
+ sign: result[4] || '',
72
+ pad: result[5] || ' ',
73
+ min: result[6] ? parseInt(result[6]) : 0,
74
+ precision: result[8] !== undefined ? parseInt(result[8]) : undefined,
75
+ code: result[9] || '%',
76
+ negative: Number(argValue) < 0,
77
+ argument: String(argValue),
78
+ });
79
+ }
80
+
81
+ strings.push(format.substring(matchPosEnd));
82
+
83
+ if (matches.length === 0) return format;
84
+ if (args.length < convCount) return format;
85
+
86
+ let output = '';
87
+
88
+ for (let i = 0; i < matches.length; i++) {
89
+ const m = matches[i]!;
90
+ let substitution: string;
91
+
92
+ switch (m.code) {
93
+ case '%':
94
+ substitution = '%';
95
+ break;
96
+ case 'b':
97
+ m.argument = Math.abs(parseInt(m.argument)).toString(2);
98
+ substitution = convert(m, true);
99
+ break;
100
+ case 'c':
101
+ m.argument = String.fromCharCode(Math.abs(parseInt(m.argument)));
102
+ substitution = convert(m, true);
103
+ break;
104
+ case 'd':
105
+ m.argument = String(Math.abs(parseInt(m.argument)));
106
+ substitution = convert(m);
107
+ break;
108
+ case 'f':
109
+ m.argument = Math.abs(parseFloat(m.argument)).toFixed(
110
+ m.precision ?? 6,
111
+ );
112
+ substitution = convert(m);
113
+ break;
114
+ case 'o':
115
+ m.argument = Math.abs(parseInt(m.argument)).toString(8);
116
+ substitution = convert(m);
117
+ break;
118
+ case 's':
119
+ m.argument = m.precision
120
+ ? m.argument.substring(0, m.precision)
121
+ : m.argument;
122
+ substitution = convert(m, true);
123
+ break;
124
+ case 'x':
125
+ m.argument = Math.abs(parseInt(m.argument)).toString(16);
126
+ substitution = convert(m);
127
+ break;
128
+ case 'X':
129
+ m.argument = Math.abs(parseInt(m.argument)).toString(16);
130
+ substitution = convert(m).toUpperCase();
131
+ break;
132
+ default:
133
+ substitution = m.match;
134
+ }
135
+
136
+ output += strings[i] + substitution;
137
+ }
138
+
139
+ output += strings[matches.length];
140
+ return output;
141
+ }
@@ -0,0 +1,168 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Constraint handlers — individual input behavior decorators
3
+ //
4
+ // Design patterns used:
5
+ // - Strategy Pattern : each constraint is an independent strategy object
6
+ // - Decorator Pattern : constraints add behavior to plain HTML inputs
7
+ // via attribute-driven binding
8
+ // - Registry Pattern : constraints are registered by name and looked up
9
+ // from the `constraint` attribute at runtime
10
+ // ---------------------------------------------------------------------------
11
+
12
+ import { sprintf } from '../utils/sprintf.js';
13
+ import { isDateValid } from '../utils/dom.js';
14
+
15
+ /**
16
+ * A constraint handler defines behavior that is attached to input elements
17
+ * bearing a matching `constraint` attribute value.
18
+ *
19
+ * Each handler is self-contained — it knows how to bind its own events
20
+ * and how to validate its own input.
21
+ */
22
+ export interface ConstraintHandler {
23
+ /** The constraint keyword (e.g. `"date"`, `"number"`). */
24
+ name: string;
25
+
26
+ /**
27
+ * Called once per matching element to attach event listeners.
28
+ * This is the **Decorator** step — it augments the element's behavior.
29
+ */
30
+ attach(element: HTMLInputElement | HTMLTextAreaElement): void;
31
+ }
32
+
33
+ // -- Date constraint --------------------------------------------------------
34
+
35
+ export const dateConstraint: ConstraintHandler = {
36
+ name: 'date',
37
+
38
+ attach(element) {
39
+ element.addEventListener('change', () => {
40
+ if (element.value !== '' && !isDateValid(element.value)) {
41
+ element.value = '';
42
+ }
43
+ });
44
+
45
+ // Disable IME for date input
46
+ (element.style as unknown as Record<string, string>)['imeMode'] = 'disabled';
47
+ element.setAttribute('inputmode', 'numeric');
48
+ },
49
+ };
50
+
51
+ // -- Number constraint ------------------------------------------------------
52
+
53
+ export const numberConstraint: ConstraintHandler = {
54
+ name: 'number',
55
+
56
+ attach(element) {
57
+ element.addEventListener('keydown', ((event: KeyboardEvent) => {
58
+ // Allow: backspace, delete, tab, escape, enter, arrows, home, end
59
+ const allowedKeys = [
60
+ 'Backspace', 'Delete', 'Tab', 'Escape', 'Enter',
61
+ 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown',
62
+ 'Home', 'End',
63
+ ];
64
+ if (allowedKeys.includes(event.key)) return;
65
+ // Allow Ctrl/Cmd + A/C/V/X
66
+ if ((event.ctrlKey || event.metaKey) && ['a', 'c', 'v', 'x'].includes(event.key)) return;
67
+ // Allow digits, dot, minus
68
+ if (/[\d.\-]/.test(event.key)) return;
69
+ event.preventDefault();
70
+ }) as EventListener);
71
+
72
+ element.addEventListener('change', () => {
73
+ const val = element.value;
74
+ if (!/^-?[0-9]+(\.[0-9]+)?$/.test(val) || /^0\d+$/.test(val)) {
75
+ element.value = '0';
76
+ }
77
+ });
78
+
79
+ // Default empty number fields to 0
80
+ if (element.value === '') {
81
+ element.value = '0';
82
+ }
83
+
84
+ (element.style as unknown as Record<string, string>)['imeMode'] = 'disabled';
85
+ element.setAttribute('inputmode', 'decimal');
86
+ },
87
+ };
88
+
89
+ // -- Time constraint --------------------------------------------------------
90
+
91
+ export const timeConstraint: ConstraintHandler = {
92
+ name: 'time',
93
+
94
+ attach(element) {
95
+ element.addEventListener('blur', () => {
96
+ const input = element.value;
97
+ let hour = '00';
98
+ let minute = '00';
99
+
100
+ if (/^\d{1,4}$/.test(input)) {
101
+ hour = sprintf('%02s', input.substring(0, 2));
102
+ minute = sprintf('%02s', input.substring(2, 4));
103
+ } else if (/^\d{0,2}:\d{0,2}$/.test(input)) {
104
+ const colonIdx = input.indexOf(':');
105
+ hour = sprintf('%02s', input.substring(0, colonIdx));
106
+ minute = sprintf('%02s', input.substring(colonIdx + 1));
107
+ }
108
+
109
+ element.value = `${hour}:${minute}`;
110
+ });
111
+
112
+ element.addEventListener('keydown', ((event: KeyboardEvent) => {
113
+ const allowedKeys = [
114
+ 'Backspace', 'Delete', 'Tab', 'Escape', 'Enter',
115
+ 'ArrowLeft', 'ArrowRight', 'Home', 'End',
116
+ ];
117
+ if (allowedKeys.includes(event.key)) return;
118
+ if ((event.ctrlKey || event.metaKey) && ['a', 'c', 'v', 'x'].includes(event.key)) return;
119
+ if (/[\d:]/.test(event.key)) return;
120
+ event.preventDefault();
121
+ }) as EventListener);
122
+
123
+ (element.style as unknown as Record<string, string>)['imeMode'] = 'disabled';
124
+ element.setAttribute('inputmode', 'numeric');
125
+ },
126
+ };
127
+
128
+ // -- UpperCase constraint ---------------------------------------------------
129
+
130
+ export const upperCaseConstraint: ConstraintHandler = {
131
+ name: 'upperCase',
132
+
133
+ attach(element) {
134
+ let timeout: ReturnType<typeof setTimeout> | undefined;
135
+
136
+ element.addEventListener('keyup', () => {
137
+ clearTimeout(timeout);
138
+ timeout = setTimeout(() => {
139
+ const val = element.value;
140
+ if (/[a-z]/.test(val)) {
141
+ element.value = val.toUpperCase();
142
+ }
143
+ }, 500);
144
+ });
145
+ },
146
+ };
147
+
148
+ // -- OnlyEn constraint ------------------------------------------------------
149
+
150
+ export const onlyEnConstraint: ConstraintHandler = {
151
+ name: 'onlyEn',
152
+
153
+ attach(element) {
154
+ (element.style as unknown as Record<string, string>)['imeMode'] = 'disabled';
155
+ element.setAttribute('inputmode', 'text');
156
+ element.setAttribute('lang', 'en');
157
+ },
158
+ };
159
+
160
+ // -- All built-in constraints -----------------------------------------------
161
+
162
+ export const builtInConstraints: ConstraintHandler[] = [
163
+ dateConstraint,
164
+ numberConstraint,
165
+ timeConstraint,
166
+ upperCaseConstraint,
167
+ onlyEnConstraint,
168
+ ];