humanjs-core 1.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/CHANGELOG.md +64 -0
- package/LICENSE +21 -0
- package/README.md +655 -0
- package/examples/api-example/app.js +365 -0
- package/examples/counter/app.js +141 -0
- package/examples/human-counter/app.human +27 -0
- package/examples/human-counter/app.js +77 -0
- package/examples/routing/app.js +129 -0
- package/examples/simple-js/app.js +30 -0
- package/examples/todo-app/app.js +378 -0
- package/examples/user-dashboard/app.js +0 -0
- package/package.json +78 -0
- package/scripts/human-compile.js +43 -0
- package/scripts/humanjs.js +700 -0
- package/src/compiler/human.js +194 -0
- package/src/core/component.js +381 -0
- package/src/core/events.js +130 -0
- package/src/core/render.js +173 -0
- package/src/core/router.js +274 -0
- package/src/core/state.js +114 -0
- package/src/index.js +61 -0
- package/src/plugins/http.js +167 -0
- package/src/plugins/storage.js +181 -0
- package/src/plugins/validator.js +193 -0
- package/src/utils/dom.js +0 -0
- package/src/utils/helpers.js +209 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
const SECTION_NAMES = ['imports', 'state', 'derived', 'actions', 'template', 'hooks'];
|
|
2
|
+
const EVENT_NAMES = ['click', 'submit', 'input', 'change', 'keydown', 'keyup', 'focus', 'blur'];
|
|
3
|
+
|
|
4
|
+
function escapeTemplateLiteral(value) {
|
|
5
|
+
return value.replace(/\\/g, '\\\\').replace(/`/g, '\\`');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function stringifyCode(code) {
|
|
9
|
+
return JSON.stringify(code);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isActionCall(value) {
|
|
13
|
+
const callMatch = value.trim().match(/^([A-Za-z_$][\w$]*)\s*\((.*)\)$/s);
|
|
14
|
+
return callMatch ? { name: callMatch[1], args: callMatch[2] } : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isSimpleActionName(value) {
|
|
18
|
+
return /^[A-Za-z_$][\w$]*$/.test(value.trim());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function compileActionAttribute(value, eventName, generatedActions) {
|
|
22
|
+
const trimmed = value.trim();
|
|
23
|
+
const actionCall = isActionCall(trimmed);
|
|
24
|
+
|
|
25
|
+
if (actionCall) {
|
|
26
|
+
return `data-${eventName}="${actionCall.name}" data-${eventName}-args='[${actionCall.args.trim()}]'`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (isSimpleActionName(trimmed)) {
|
|
30
|
+
return `data-${eventName}="${trimmed}"`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const generatedName = `__inline_${generatedActions.length + 1}`;
|
|
34
|
+
generatedActions.push({
|
|
35
|
+
name: generatedName,
|
|
36
|
+
code: trimmed
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return `data-${eventName}="${generatedName}"`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function parseHumanFile(source) {
|
|
43
|
+
const normalized = source.replace(/\r\n/g, '\n');
|
|
44
|
+
const sections = Object.fromEntries(SECTION_NAMES.map((name) => [name, '']));
|
|
45
|
+
const lines = normalized.split('\n');
|
|
46
|
+
let currentSection = null;
|
|
47
|
+
let foundSection = false;
|
|
48
|
+
|
|
49
|
+
lines.forEach((line) => {
|
|
50
|
+
const headerMatch = line.match(/^---([a-z]+)$/);
|
|
51
|
+
|
|
52
|
+
if (headerMatch) {
|
|
53
|
+
const [, name] = headerMatch;
|
|
54
|
+
if (!SECTION_NAMES.includes(name)) {
|
|
55
|
+
throw new Error(`[HumanJS] Unknown .human section: ${name}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
currentSection = name;
|
|
59
|
+
foundSection = true;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!currentSection) return;
|
|
64
|
+
sections[currentSection] += `${line}\n`;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!foundSection) {
|
|
68
|
+
throw new Error('[HumanJS] Invalid .human file. Use sections like ---state and ---template.');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
Object.keys(sections).forEach((name) => {
|
|
72
|
+
sections[name] = sections[name].trim();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (!sections.template) {
|
|
76
|
+
throw new Error('[HumanJS] .human files require a ---template section.');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return sections;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function compileTemplate(template) {
|
|
83
|
+
const generatedActions = [];
|
|
84
|
+
let compiled = escapeTemplateLiteral(template.trim());
|
|
85
|
+
|
|
86
|
+
compiled = compiled.replace(
|
|
87
|
+
/\{([^{}]+)\}/g,
|
|
88
|
+
(_, expression) => `\${__humanExpr(state, actions, ${stringifyCode(expression.trim())})}`
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
EVENT_NAMES.forEach((eventName) => {
|
|
92
|
+
const pattern = new RegExp(`@${eventName}="([^"]+)"`, 'g');
|
|
93
|
+
compiled = compiled.replace(pattern, (_, value) => compileActionAttribute(value, eventName, generatedActions));
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return { template: compiled, generatedActions };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function humanExpr(state, actions, code) {
|
|
100
|
+
return new Function('state', 'actions', `with (state) { return (${code}); }`)(state, actions);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function humanRun(state, actions, event, el, args, code) {
|
|
104
|
+
return new Function(
|
|
105
|
+
'state',
|
|
106
|
+
'actions',
|
|
107
|
+
'event',
|
|
108
|
+
'el',
|
|
109
|
+
'args',
|
|
110
|
+
`with (state) { ${code}; }`
|
|
111
|
+
)(state, actions, event, el, args);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function createRuntimeTemplate(template) {
|
|
115
|
+
const { template: compiledTemplate, generatedActions } = compileTemplate(template);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
render({ html, state, actions }) {
|
|
119
|
+
return new Function(
|
|
120
|
+
'html',
|
|
121
|
+
'state',
|
|
122
|
+
'actions',
|
|
123
|
+
'__humanExpr',
|
|
124
|
+
`return html\`${compiledTemplate}\`;`
|
|
125
|
+
)(html, state, actions, humanExpr);
|
|
126
|
+
},
|
|
127
|
+
actions: Object.fromEntries(
|
|
128
|
+
generatedActions.map(({ name, code }) => [
|
|
129
|
+
name,
|
|
130
|
+
({ state, actions, event, el, args }) => humanRun(state, actions, event, el, args, code)
|
|
131
|
+
])
|
|
132
|
+
)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function compileHuman(source, options = {}) {
|
|
137
|
+
const sections = parseHumanFile(source);
|
|
138
|
+
const appImport = options.appImportPath || '../../src/index.js';
|
|
139
|
+
const imports = sections.imports ? `${sections.imports}\n\n` : '';
|
|
140
|
+
const state = sections.state || '{}';
|
|
141
|
+
const derived = sections.derived || '{}';
|
|
142
|
+
const rawActions = sections.actions.trim();
|
|
143
|
+
const actions = rawActions || '{}';
|
|
144
|
+
const hooks = sections.hooks || '';
|
|
145
|
+
const { template, generatedActions } = compileTemplate(sections.template);
|
|
146
|
+
const hasCustomActions = rawActions && rawActions !== '{}';
|
|
147
|
+
const mergedActions = generatedActions.length
|
|
148
|
+
? `{
|
|
149
|
+
${generatedActions.map(({ name, code }) => `
|
|
150
|
+
${name}({ state, actions, event, el, args }) {
|
|
151
|
+
return __humanRun(state, actions, event, el, args, ${stringifyCode(code)});
|
|
152
|
+
}`).join(',\n')}${hasCustomActions ? ',\n' : ''}
|
|
153
|
+
${hasCustomActions ? rawActions.slice(1, -1).trim() : ''}
|
|
154
|
+
}`
|
|
155
|
+
: actions;
|
|
156
|
+
|
|
157
|
+
return `${imports}import { app, html } from '${appImport}';
|
|
158
|
+
|
|
159
|
+
const __humanExpr = ${humanExpr.toString()};
|
|
160
|
+
|
|
161
|
+
const __humanRun = ${humanRun.toString()};
|
|
162
|
+
|
|
163
|
+
app.human({
|
|
164
|
+
state: ${state},
|
|
165
|
+
derived: ${derived},
|
|
166
|
+
actions: ${mergedActions},
|
|
167
|
+
render: ({ state, actions }) => html\`
|
|
168
|
+
${template}
|
|
169
|
+
\`${hooks ? `,\n${hooks}` : ''}
|
|
170
|
+
});
|
|
171
|
+
`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function loadHuman(url, options = {}) {
|
|
175
|
+
const response = await fetch(url);
|
|
176
|
+
|
|
177
|
+
if (!response.ok) {
|
|
178
|
+
throw new Error(`[HumanJS] Failed to load ${url}: ${response.status}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const source = await response.text();
|
|
182
|
+
const compiled = compileHuman(source, {
|
|
183
|
+
appImportPath: options.appImportPath || new URL('../index.js', import.meta.url).href
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const blob = new Blob([compiled], { type: 'text/javascript' });
|
|
187
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
return await import(blobUrl);
|
|
191
|
+
} finally {
|
|
192
|
+
URL.revokeObjectURL(blobUrl);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* COMPONENT SYSTEM
|
|
3
|
+
*
|
|
4
|
+
* Create reactive components with state, lifecycle hooks, and auto re-rendering.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { createState } from './state.js';
|
|
8
|
+
import { html } from './render.js';
|
|
9
|
+
import { attachEvents } from './events.js';
|
|
10
|
+
import { createRuntimeTemplate } from '../compiler/human.js';
|
|
11
|
+
|
|
12
|
+
const ACTION_EVENTS = [
|
|
13
|
+
'click',
|
|
14
|
+
'submit',
|
|
15
|
+
'input',
|
|
16
|
+
'change',
|
|
17
|
+
'keydown',
|
|
18
|
+
'keyup',
|
|
19
|
+
'focus',
|
|
20
|
+
'blur'
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create a reactive component
|
|
25
|
+
* @param {HTMLElement} rootElement - Where to mount the component
|
|
26
|
+
* @param {Function} renderFn - Function that returns { element, events }
|
|
27
|
+
* @param {Object} initialState - Initial state
|
|
28
|
+
* @param {Object} lifecycle - { onMount, onUpdate, onDestroy }
|
|
29
|
+
*/
|
|
30
|
+
export function createComponent(rootElement, renderFn, initialState = {}, initialLifecycle = {}) {
|
|
31
|
+
let currentElement = null;
|
|
32
|
+
let cleanupEvents = null;
|
|
33
|
+
let cleanupRender = null;
|
|
34
|
+
let isDestroyed = false;
|
|
35
|
+
|
|
36
|
+
// Create reactive state FIRST
|
|
37
|
+
const componentState = createState(initialState);
|
|
38
|
+
|
|
39
|
+
// Render function
|
|
40
|
+
function render() {
|
|
41
|
+
if (isDestroyed) return;
|
|
42
|
+
const previousCleanupRender = cleanupRender;
|
|
43
|
+
|
|
44
|
+
// Call render function with state
|
|
45
|
+
const result = renderFn(componentState);
|
|
46
|
+
|
|
47
|
+
// Extract element and events
|
|
48
|
+
let newElement, events;
|
|
49
|
+
if (result && typeof result === 'object' && result.element) {
|
|
50
|
+
newElement = result.element;
|
|
51
|
+
events = result.events || {};
|
|
52
|
+
cleanupRender = typeof result.cleanup === 'function' ? result.cleanup : null;
|
|
53
|
+
} else {
|
|
54
|
+
newElement = result;
|
|
55
|
+
events = {};
|
|
56
|
+
cleanupRender = null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// First render - mount
|
|
60
|
+
if (!currentElement) {
|
|
61
|
+
currentElement = newElement;
|
|
62
|
+
rootElement.appendChild(currentElement);
|
|
63
|
+
|
|
64
|
+
// Attach events
|
|
65
|
+
cleanupEvents = attachEvents(currentElement, events);
|
|
66
|
+
|
|
67
|
+
// Call onMount
|
|
68
|
+
if (initialLifecycle.onMount) {
|
|
69
|
+
initialLifecycle.onMount(componentState);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Subsequent renders - replace
|
|
73
|
+
else {
|
|
74
|
+
// Cleanup old events
|
|
75
|
+
if (cleanupEvents) cleanupEvents();
|
|
76
|
+
if (previousCleanupRender) previousCleanupRender();
|
|
77
|
+
|
|
78
|
+
// Replace element
|
|
79
|
+
rootElement.replaceChild(newElement, currentElement);
|
|
80
|
+
currentElement = newElement;
|
|
81
|
+
|
|
82
|
+
// Attach new events
|
|
83
|
+
cleanupEvents = attachEvents(currentElement, events);
|
|
84
|
+
|
|
85
|
+
// Call onUpdate
|
|
86
|
+
if (initialLifecycle.onUpdate) {
|
|
87
|
+
initialLifecycle.onUpdate(componentState);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Watch for state changes and re-render
|
|
93
|
+
componentState.$watch = new Proxy(componentState.$watch || (() => {}), {
|
|
94
|
+
apply(target, thisArg, args) {
|
|
95
|
+
const result = target.apply(thisArg, args);
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Override state setter to trigger re-render
|
|
101
|
+
const originalState = { ...componentState.$raw() };
|
|
102
|
+
Object.keys(originalState).forEach(key => {
|
|
103
|
+
let value = componentState[key];
|
|
104
|
+
Object.defineProperty(componentState, key, {
|
|
105
|
+
get() {
|
|
106
|
+
return value;
|
|
107
|
+
},
|
|
108
|
+
set(newValue) {
|
|
109
|
+
if (value !== newValue) {
|
|
110
|
+
value = newValue;
|
|
111
|
+
render();
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
enumerable: true,
|
|
115
|
+
configurable: true
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Destroy function
|
|
120
|
+
function destroy() {
|
|
121
|
+
if (isDestroyed) return;
|
|
122
|
+
|
|
123
|
+
isDestroyed = true;
|
|
124
|
+
|
|
125
|
+
// Cleanup events
|
|
126
|
+
if (cleanupEvents) cleanupEvents();
|
|
127
|
+
if (cleanupRender) cleanupRender();
|
|
128
|
+
|
|
129
|
+
// Call onDestroy
|
|
130
|
+
if (initialLifecycle.onDestroy) {
|
|
131
|
+
initialLifecycle.onDestroy(componentState);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Remove from DOM
|
|
135
|
+
if (currentElement && currentElement.parentNode) {
|
|
136
|
+
currentElement.parentNode.removeChild(currentElement);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
currentElement = null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Initial render
|
|
143
|
+
render();
|
|
144
|
+
|
|
145
|
+
// Return component API
|
|
146
|
+
return {
|
|
147
|
+
state: componentState,
|
|
148
|
+
destroy,
|
|
149
|
+
render,
|
|
150
|
+
get element() {
|
|
151
|
+
return currentElement;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function parseActionArgs(rawValue) {
|
|
157
|
+
if (!rawValue) return [];
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const parsed = JSON.parse(rawValue);
|
|
161
|
+
return Array.isArray(parsed) ? parsed : [parsed];
|
|
162
|
+
} catch {
|
|
163
|
+
return [rawValue];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function createActionBindings(rootElement, actions, getContext) {
|
|
168
|
+
const cleanups = [];
|
|
169
|
+
|
|
170
|
+
ACTION_EVENTS.forEach((eventName) => {
|
|
171
|
+
const attrName = `data-${eventName}`;
|
|
172
|
+
const listener = (event) => {
|
|
173
|
+
const target = event.target.closest(`[${attrName}]`);
|
|
174
|
+
if (!target || !rootElement.contains(target)) return;
|
|
175
|
+
|
|
176
|
+
const actionName = target.getAttribute(attrName);
|
|
177
|
+
const action = actions[actionName];
|
|
178
|
+
|
|
179
|
+
if (typeof action !== 'function') {
|
|
180
|
+
console.warn(`[HumanJS] Unknown action: ${actionName}`);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const args = parseActionArgs(target.getAttribute(`${attrName}-args`));
|
|
185
|
+
const context = getContext(target, event);
|
|
186
|
+
|
|
187
|
+
action({ ...context, args });
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
rootElement.addEventListener(eventName, listener);
|
|
191
|
+
cleanups.push(() => rootElement.removeEventListener(eventName, listener));
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
return () => cleanups.forEach((cleanup) => cleanup());
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function createApp(options = {}) {
|
|
198
|
+
const {
|
|
199
|
+
root = document.getElementById('app'),
|
|
200
|
+
state: initialState = {},
|
|
201
|
+
derived = {},
|
|
202
|
+
actions: actionDefinitions = {},
|
|
203
|
+
render,
|
|
204
|
+
onMount,
|
|
205
|
+
onUpdate,
|
|
206
|
+
onDestroy
|
|
207
|
+
} = options;
|
|
208
|
+
|
|
209
|
+
if (!render) {
|
|
210
|
+
throw new Error('[HumanJS] render function is required');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!root) {
|
|
214
|
+
throw new Error('[HumanJS] root element is required');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let instance = null;
|
|
218
|
+
let derivedReady = false;
|
|
219
|
+
const boundActions = Object.fromEntries(
|
|
220
|
+
Object.entries(actionDefinitions).map(([name, action]) => [
|
|
221
|
+
name,
|
|
222
|
+
(...args) => action({
|
|
223
|
+
state: instance.state,
|
|
224
|
+
actions: boundActions,
|
|
225
|
+
root,
|
|
226
|
+
render: () => instance.render(),
|
|
227
|
+
args
|
|
228
|
+
})
|
|
229
|
+
])
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const lifecycle = {
|
|
233
|
+
onMount(state) {
|
|
234
|
+
if (onMount) {
|
|
235
|
+
onMount({ state, actions: boundActions, root });
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
onUpdate(state) {
|
|
239
|
+
if (onUpdate) {
|
|
240
|
+
onUpdate({ state, actions: boundActions, root });
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
onDestroy(state) {
|
|
244
|
+
if (onDestroy) {
|
|
245
|
+
onDestroy({ state, actions: boundActions, root });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
instance = createComponent(
|
|
251
|
+
root,
|
|
252
|
+
(state) => {
|
|
253
|
+
if (!derivedReady) {
|
|
254
|
+
Object.entries(derived).forEach(([key, compute]) => {
|
|
255
|
+
if (typeof compute === 'function') {
|
|
256
|
+
state.$computed(key, () => compute(state, boundActions));
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
derivedReady = true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const viewContext = { state, actions: boundActions, root };
|
|
263
|
+
const result = render(viewContext);
|
|
264
|
+
|
|
265
|
+
let element;
|
|
266
|
+
let events = {};
|
|
267
|
+
|
|
268
|
+
if (result && typeof result === 'object' && result.element) {
|
|
269
|
+
element = result.element;
|
|
270
|
+
events = result.events || {};
|
|
271
|
+
} else {
|
|
272
|
+
element = result;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const cleanupActions = createActionBindings(element, boundActions, (target, event) => ({
|
|
276
|
+
state,
|
|
277
|
+
actions: boundActions,
|
|
278
|
+
root,
|
|
279
|
+
event,
|
|
280
|
+
el: target,
|
|
281
|
+
render: () => instance.render()
|
|
282
|
+
}));
|
|
283
|
+
|
|
284
|
+
const cleanupEvents = attachEvents(element, events);
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
element,
|
|
288
|
+
events: {},
|
|
289
|
+
cleanup() {
|
|
290
|
+
cleanupActions();
|
|
291
|
+
cleanupEvents();
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
},
|
|
295
|
+
initialState,
|
|
296
|
+
lifecycle
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
...instance,
|
|
301
|
+
actions: boundActions
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function createSimpleApp(options = {}) {
|
|
306
|
+
const {
|
|
307
|
+
template,
|
|
308
|
+
actions = {},
|
|
309
|
+
...rest
|
|
310
|
+
} = options;
|
|
311
|
+
|
|
312
|
+
if (!template) {
|
|
313
|
+
throw new Error('[HumanJS] template is required');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const runtime = createRuntimeTemplate(template);
|
|
317
|
+
|
|
318
|
+
return createApp({
|
|
319
|
+
...rest,
|
|
320
|
+
actions: {
|
|
321
|
+
...runtime.actions,
|
|
322
|
+
...actions
|
|
323
|
+
},
|
|
324
|
+
render: ({ state, actions, root }) => runtime.render({ html, state, actions, root })
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Higher-level app API for cleaner component creation
|
|
330
|
+
*/
|
|
331
|
+
export const app = {
|
|
332
|
+
/**
|
|
333
|
+
* Create a component with options object
|
|
334
|
+
*/
|
|
335
|
+
create(options = {}) {
|
|
336
|
+
const {
|
|
337
|
+
root = document.getElementById('app'),
|
|
338
|
+
state: initialState = {},
|
|
339
|
+
render: renderFn,
|
|
340
|
+
onMount,
|
|
341
|
+
onUpdate,
|
|
342
|
+
onDestroy
|
|
343
|
+
} = options;
|
|
344
|
+
|
|
345
|
+
if (!renderFn) {
|
|
346
|
+
throw new Error('[HumanJS] render function is required');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return createComponent(
|
|
350
|
+
root,
|
|
351
|
+
renderFn,
|
|
352
|
+
initialState,
|
|
353
|
+
{ onMount, onUpdate, onDestroy }
|
|
354
|
+
);
|
|
355
|
+
},
|
|
356
|
+
|
|
357
|
+
human(options = {}) {
|
|
358
|
+
return createApp(options);
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
simple(options = {}) {
|
|
362
|
+
return createSimpleApp(options);
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Quick mount without state management
|
|
367
|
+
*/
|
|
368
|
+
mount(rootOrFn, renderFn) {
|
|
369
|
+
let root, render;
|
|
370
|
+
|
|
371
|
+
if (typeof rootOrFn === 'function') {
|
|
372
|
+
root = document.getElementById('app');
|
|
373
|
+
render = rootOrFn;
|
|
374
|
+
} else {
|
|
375
|
+
root = rootOrFn;
|
|
376
|
+
render = renderFn;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return createComponent(root, () => render(), {});
|
|
380
|
+
}
|
|
381
|
+
};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EVENT HANDLING SYSTEM
|
|
3
|
+
*
|
|
4
|
+
* Attach and manage DOM event listeners.
|
|
5
|
+
* Supports event delegation and automatic cleanup.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Attach events to DOM elements
|
|
10
|
+
* @param {HTMLElement} rootElement
|
|
11
|
+
* @param {Object} eventMap - { selector: { eventType: handler } }
|
|
12
|
+
*/
|
|
13
|
+
export function attachEvents(rootElement, eventMap = {}) {
|
|
14
|
+
const attachedListeners = [];
|
|
15
|
+
|
|
16
|
+
Object.keys(eventMap).forEach(selector => {
|
|
17
|
+
const events = eventMap[selector];
|
|
18
|
+
|
|
19
|
+
// Find target elements
|
|
20
|
+
let targetElements;
|
|
21
|
+
if (selector === 'root' || selector === ':root') {
|
|
22
|
+
targetElements = [rootElement];
|
|
23
|
+
} else {
|
|
24
|
+
targetElements = [...rootElement.querySelectorAll(selector)];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!targetElements || targetElements.length === 0) {
|
|
28
|
+
console.warn(`[HumanJS] Element not found: ${selector}`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
targetElements.forEach((targetElement) => {
|
|
33
|
+
Object.keys(events).forEach(eventType => {
|
|
34
|
+
const handler = events[eventType];
|
|
35
|
+
targetElement.addEventListener(eventType, handler);
|
|
36
|
+
|
|
37
|
+
attachedListeners.push({
|
|
38
|
+
element: targetElement,
|
|
39
|
+
type: eventType,
|
|
40
|
+
handler
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Return cleanup function
|
|
47
|
+
return () => {
|
|
48
|
+
attachedListeners.forEach(({ element, type, handler }) => {
|
|
49
|
+
element.removeEventListener(type, handler);
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Event delegation - attach one listener to parent
|
|
56
|
+
* @param {HTMLElement} parent
|
|
57
|
+
* @param {String} eventType
|
|
58
|
+
* @param {String} selector
|
|
59
|
+
* @param {Function} handler
|
|
60
|
+
*/
|
|
61
|
+
export function delegate(parent, eventType, selector, handler) {
|
|
62
|
+
const listener = (event) => {
|
|
63
|
+
const target = event.target.closest(selector);
|
|
64
|
+
if (target && parent.contains(target)) {
|
|
65
|
+
handler.call(target, event);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
parent.addEventListener(eventType, listener);
|
|
70
|
+
|
|
71
|
+
return () => parent.removeEventListener(eventType, listener);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Debounce function calls
|
|
76
|
+
* @param {Function} fn
|
|
77
|
+
* @param {Number} delay
|
|
78
|
+
*/
|
|
79
|
+
export function debounce(fn, delay = 300) {
|
|
80
|
+
let timeoutId;
|
|
81
|
+
return function(...args) {
|
|
82
|
+
clearTimeout(timeoutId);
|
|
83
|
+
timeoutId = setTimeout(() => fn.apply(this, args), delay);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Throttle function calls
|
|
89
|
+
* @param {Function} fn
|
|
90
|
+
* @param {Number} limit
|
|
91
|
+
*/
|
|
92
|
+
export function throttle(fn, limit = 300) {
|
|
93
|
+
let inThrottle;
|
|
94
|
+
return function(...args) {
|
|
95
|
+
if (!inThrottle) {
|
|
96
|
+
fn.apply(this, args);
|
|
97
|
+
inThrottle = true;
|
|
98
|
+
setTimeout(() => inThrottle = false, limit);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Common event helpers
|
|
105
|
+
*/
|
|
106
|
+
export const on = {
|
|
107
|
+
click: (selector, handler) => ({ [selector]: { click: handler } }),
|
|
108
|
+
input: (selector, handler) => ({ [selector]: { input: handler } }),
|
|
109
|
+
submit: (selector, handler) => ({
|
|
110
|
+
[selector]: {
|
|
111
|
+
submit: (e) => {
|
|
112
|
+
e.preventDefault();
|
|
113
|
+
handler(e);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}),
|
|
117
|
+
change: (selector, handler) => ({ [selector]: { change: handler } }),
|
|
118
|
+
focus: (selector, handler) => ({ [selector]: { focus: handler } }),
|
|
119
|
+
blur: (selector, handler) => ({ [selector]: { blur: handler } }),
|
|
120
|
+
keydown: (selector, handler) => ({ [selector]: { keydown: handler } }),
|
|
121
|
+
keyup: (selector, handler) => ({ [selector]: { keyup: handler } })
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Merge multiple event maps
|
|
126
|
+
* @param {...Object} eventMaps
|
|
127
|
+
*/
|
|
128
|
+
export function mergeEvents(...eventMaps) {
|
|
129
|
+
return Object.assign({}, ...eventMaps);
|
|
130
|
+
}
|