@kaiserofthenight/human-js 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/README.md +545 -0
- package/examples/api-example/app.js +365 -0
- package/examples/counter/app.js +201 -0
- package/examples/todo-app/app.js +378 -0
- package/examples/user-dashboard/app.js +0 -0
- package/package.json +66 -0
- package/src/core/component.js +182 -0
- package/src/core/events.js +130 -0
- package/src/core/render.js +151 -0
- package/src/core/router.js +182 -0
- package/src/core/state.js +114 -0
- package/src/index.js +63 -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,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 element
|
|
20
|
+
let targetElement;
|
|
21
|
+
if (selector === 'root' || selector === ':root') {
|
|
22
|
+
targetElement = rootElement;
|
|
23
|
+
} else {
|
|
24
|
+
targetElement = rootElement.querySelector(selector);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!targetElement) {
|
|
28
|
+
console.warn(`[HumanJS] Element not found: ${selector}`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Attach each event type
|
|
33
|
+
Object.keys(events).forEach(eventType => {
|
|
34
|
+
const handler = events[eventType];
|
|
35
|
+
targetElement.addEventListener(eventType, handler);
|
|
36
|
+
|
|
37
|
+
// Store for cleanup
|
|
38
|
+
attachedListeners.push({
|
|
39
|
+
element: targetElement,
|
|
40
|
+
type: eventType,
|
|
41
|
+
handler
|
|
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
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML RENDERING SYSTEM
|
|
3
|
+
*
|
|
4
|
+
* Convert tagged template literals into real DOM elements.
|
|
5
|
+
* Supports dynamic values, nested elements, and arrays.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Main html tagged template function
|
|
10
|
+
* @param {Array} strings - Template string parts
|
|
11
|
+
* @param {...any} values - Dynamic values
|
|
12
|
+
* @returns {HTMLElement} DOM element
|
|
13
|
+
*/
|
|
14
|
+
export function html(strings, ...values) {
|
|
15
|
+
// Build complete HTML string
|
|
16
|
+
let htmlString = '';
|
|
17
|
+
|
|
18
|
+
strings.forEach((str, i) => {
|
|
19
|
+
htmlString += str;
|
|
20
|
+
|
|
21
|
+
if (i < values.length) {
|
|
22
|
+
const value = values[i];
|
|
23
|
+
|
|
24
|
+
// Handle different value types
|
|
25
|
+
if (value === null || value === undefined) {
|
|
26
|
+
htmlString += '';
|
|
27
|
+
} else if (Array.isArray(value)) {
|
|
28
|
+
// For arrays, join DOM elements
|
|
29
|
+
htmlString += value.map(v => {
|
|
30
|
+
if (v instanceof HTMLElement) {
|
|
31
|
+
return v.outerHTML;
|
|
32
|
+
}
|
|
33
|
+
return String(v);
|
|
34
|
+
}).join('');
|
|
35
|
+
} else if (value instanceof HTMLElement) {
|
|
36
|
+
htmlString += value.outerHTML;
|
|
37
|
+
} else {
|
|
38
|
+
htmlString += String(value);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Create DOM from string
|
|
44
|
+
return createElementFromHTML(htmlString.trim());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Create a DOM element from HTML string
|
|
49
|
+
* @param {String} htmlString - HTML markup
|
|
50
|
+
* @returns {HTMLElement} DOM element
|
|
51
|
+
*/
|
|
52
|
+
function createElementFromHTML(htmlString) {
|
|
53
|
+
const template = document.createElement('template');
|
|
54
|
+
template.innerHTML = htmlString;
|
|
55
|
+
|
|
56
|
+
const element = template.content.firstChild;
|
|
57
|
+
|
|
58
|
+
if (!element) {
|
|
59
|
+
throw new Error('Invalid HTML: ' + htmlString);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return element;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Create element with props (alternative to html``)
|
|
67
|
+
* @param {String} tag - Element tag name
|
|
68
|
+
* @param {Object} props - Element properties
|
|
69
|
+
* @param {...any} children - Child elements
|
|
70
|
+
* @returns {HTMLElement}
|
|
71
|
+
*/
|
|
72
|
+
export function createElement(tag, props = {}, ...children) {
|
|
73
|
+
const element = document.createElement(tag);
|
|
74
|
+
|
|
75
|
+
// Apply props
|
|
76
|
+
Object.keys(props).forEach(key => {
|
|
77
|
+
if (key === 'className') {
|
|
78
|
+
element.className = props[key];
|
|
79
|
+
} else if (key === 'style' && typeof props[key] === 'object') {
|
|
80
|
+
Object.assign(element.style, props[key]);
|
|
81
|
+
} else if (key.startsWith('on')) {
|
|
82
|
+
const eventName = key.substring(2).toLowerCase();
|
|
83
|
+
element.addEventListener(eventName, props[key]);
|
|
84
|
+
} else {
|
|
85
|
+
element.setAttribute(key, props[key]);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Append children
|
|
90
|
+
children.forEach(child => {
|
|
91
|
+
if (child === null || child === undefined) return;
|
|
92
|
+
|
|
93
|
+
if (typeof child === 'string') {
|
|
94
|
+
element.appendChild(document.createTextNode(child));
|
|
95
|
+
} else if (child instanceof HTMLElement) {
|
|
96
|
+
element.appendChild(child);
|
|
97
|
+
} else if (Array.isArray(child)) {
|
|
98
|
+
child.forEach(c => {
|
|
99
|
+
if (c instanceof HTMLElement) {
|
|
100
|
+
element.appendChild(c);
|
|
101
|
+
} else {
|
|
102
|
+
element.appendChild(document.createTextNode(String(c)));
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
} else {
|
|
106
|
+
element.appendChild(document.createTextNode(String(child)));
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return element;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Render multiple children
|
|
115
|
+
* @param {...any} children
|
|
116
|
+
* @returns {DocumentFragment}
|
|
117
|
+
*/
|
|
118
|
+
export function fragment(...children) {
|
|
119
|
+
const frag = document.createDocumentFragment();
|
|
120
|
+
|
|
121
|
+
children.forEach(child => {
|
|
122
|
+
if (child instanceof HTMLElement) {
|
|
123
|
+
frag.appendChild(child);
|
|
124
|
+
} else if (typeof child === 'string') {
|
|
125
|
+
frag.appendChild(document.createTextNode(child));
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return frag;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Conditionally render elements
|
|
134
|
+
* @param {Boolean} condition
|
|
135
|
+
* @param {Function} trueRender
|
|
136
|
+
* @param {Function} falseRender
|
|
137
|
+
* @returns {HTMLElement}
|
|
138
|
+
*/
|
|
139
|
+
export function when(condition, trueRender, falseRender = () => html`<span></span>`) {
|
|
140
|
+
return condition ? trueRender() : falseRender();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Render list of items
|
|
145
|
+
* @param {Array} items
|
|
146
|
+
* @param {Function} renderItem
|
|
147
|
+
* @returns {Array<HTMLElement>}
|
|
148
|
+
*/
|
|
149
|
+
export function each(items, renderItem) {
|
|
150
|
+
return items.map((item, index) => renderItem(item, index));
|
|
151
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPA ROUTING SYSTEM
|
|
3
|
+
*
|
|
4
|
+
* Hash-based routing for single page applications.
|
|
5
|
+
* No server configuration required.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create a router
|
|
10
|
+
* @param {Object} routes - Route definitions { '/path': component }
|
|
11
|
+
* @param {Object} options - Router options
|
|
12
|
+
*/
|
|
13
|
+
export function createRouter(routes, options = {}) {
|
|
14
|
+
const {
|
|
15
|
+
root = document.getElementById('app'),
|
|
16
|
+
notFound = () => {
|
|
17
|
+
const el = document.createElement('div');
|
|
18
|
+
el.innerHTML = '<h1>404 - Page Not Found</h1>';
|
|
19
|
+
return el;
|
|
20
|
+
},
|
|
21
|
+
beforeEach = null,
|
|
22
|
+
afterEach = null
|
|
23
|
+
} = options;
|
|
24
|
+
|
|
25
|
+
let currentRoute = null;
|
|
26
|
+
let currentComponent = null;
|
|
27
|
+
|
|
28
|
+
// Parse route params (e.g., /user/:id)
|
|
29
|
+
function matchRoute(hash) {
|
|
30
|
+
const path = hash.slice(1) || '/';
|
|
31
|
+
|
|
32
|
+
// Try exact match first
|
|
33
|
+
if (routes[path]) {
|
|
34
|
+
return { route: path, params: {} };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Try parameterized routes
|
|
38
|
+
for (const route in routes) {
|
|
39
|
+
const paramNames = [];
|
|
40
|
+
const regexPattern = route.replace(/:([^/]+)/g, (_, paramName) => {
|
|
41
|
+
paramNames.push(paramName);
|
|
42
|
+
return '([^/]+)';
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
46
|
+
const match = path.match(regex);
|
|
47
|
+
|
|
48
|
+
if (match) {
|
|
49
|
+
const params = {};
|
|
50
|
+
paramNames.forEach((name, index) => {
|
|
51
|
+
params[name] = match[index + 1];
|
|
52
|
+
});
|
|
53
|
+
return { route, params };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Navigate to a route
|
|
61
|
+
function navigate(path, replace = false) {
|
|
62
|
+
if (replace) {
|
|
63
|
+
window.location.replace(`#${path}`);
|
|
64
|
+
} else {
|
|
65
|
+
window.location.hash = path;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Render current route
|
|
70
|
+
function render() {
|
|
71
|
+
const hash = window.location.hash;
|
|
72
|
+
const match = matchRoute(hash);
|
|
73
|
+
|
|
74
|
+
let route, params;
|
|
75
|
+
if (match) {
|
|
76
|
+
route = match.route;
|
|
77
|
+
params = match.params;
|
|
78
|
+
} else {
|
|
79
|
+
route = '*';
|
|
80
|
+
params = {};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Call beforeEach guard
|
|
84
|
+
if (beforeEach) {
|
|
85
|
+
const result = beforeEach(route, currentRoute, params);
|
|
86
|
+
if (result === false) return; // Cancel navigation
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Get route component
|
|
90
|
+
const routeComponent = routes[route] || notFound;
|
|
91
|
+
|
|
92
|
+
// Destroy previous component
|
|
93
|
+
if (currentComponent && currentComponent.destroy) {
|
|
94
|
+
currentComponent.destroy();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Clear root
|
|
98
|
+
root.innerHTML = '';
|
|
99
|
+
|
|
100
|
+
// Render new component
|
|
101
|
+
if (typeof routeComponent === 'function') {
|
|
102
|
+
const element = routeComponent(params);
|
|
103
|
+
|
|
104
|
+
if (element instanceof HTMLElement) {
|
|
105
|
+
root.appendChild(element);
|
|
106
|
+
} else if (element && element.element) {
|
|
107
|
+
currentComponent = element;
|
|
108
|
+
root.appendChild(element.element);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const previousRoute = currentRoute;
|
|
113
|
+
currentRoute = route;
|
|
114
|
+
|
|
115
|
+
// Call afterEach hook
|
|
116
|
+
if (afterEach) {
|
|
117
|
+
afterEach(route, previousRoute, params);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Listen for hash changes
|
|
122
|
+
window.addEventListener('hashchange', render);
|
|
123
|
+
|
|
124
|
+
// Initial render
|
|
125
|
+
render();
|
|
126
|
+
|
|
127
|
+
// Return router API
|
|
128
|
+
return {
|
|
129
|
+
navigate,
|
|
130
|
+
back: () => window.history.back(),
|
|
131
|
+
forward: () => window.history.forward(),
|
|
132
|
+
getCurrentRoute: () => currentRoute,
|
|
133
|
+
destroy: () => {
|
|
134
|
+
window.removeEventListener('hashchange', render);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Link component for navigation
|
|
141
|
+
*/
|
|
142
|
+
export function Link(to, text, className = '') {
|
|
143
|
+
const link = document.createElement('a');
|
|
144
|
+
link.href = `#${to}`;
|
|
145
|
+
link.textContent = text;
|
|
146
|
+
link.className = className;
|
|
147
|
+
|
|
148
|
+
// Prevent default and use router navigation
|
|
149
|
+
link.addEventListener('click', (e) => {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
window.location.hash = to;
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
return link;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get route params from URL
|
|
159
|
+
*/
|
|
160
|
+
export function getParams() {
|
|
161
|
+
const hash = window.location.hash.slice(1);
|
|
162
|
+
const [path, query] = hash.split('?');
|
|
163
|
+
|
|
164
|
+
if (!query) return {};
|
|
165
|
+
|
|
166
|
+
return query.split('&').reduce((params, param) => {
|
|
167
|
+
const [key, value] = param.split('=');
|
|
168
|
+
params[decodeURIComponent(key)] = decodeURIComponent(value || '');
|
|
169
|
+
return params;
|
|
170
|
+
}, {});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Build URL with query params
|
|
175
|
+
*/
|
|
176
|
+
export function buildUrl(path, params = {}) {
|
|
177
|
+
const query = Object.keys(params)
|
|
178
|
+
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
|
|
179
|
+
.join('&');
|
|
180
|
+
|
|
181
|
+
return query ? `${path}?${query}` : path;
|
|
182
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REACTIVE STATE MANAGEMENT
|
|
3
|
+
*
|
|
4
|
+
* Creates observable state objects that trigger callbacks on changes.
|
|
5
|
+
* Uses JavaScript Proxy to intercept property access and modifications.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create a reactive state object
|
|
10
|
+
* @param {Object} initialState - Initial state values
|
|
11
|
+
* @param {Function} onChange - Callback fired on any state change
|
|
12
|
+
* @returns {Proxy} Reactive state object
|
|
13
|
+
*/
|
|
14
|
+
export function createState(initialState = {}, onChange = null) {
|
|
15
|
+
// Store the raw state
|
|
16
|
+
const state = { ...initialState };
|
|
17
|
+
|
|
18
|
+
// Store computed properties
|
|
19
|
+
const computed = {};
|
|
20
|
+
|
|
21
|
+
// Store watchers for specific properties
|
|
22
|
+
const watchers = {};
|
|
23
|
+
|
|
24
|
+
// Create reactive proxy
|
|
25
|
+
const proxy = new Proxy(state, {
|
|
26
|
+
get(target, property) {
|
|
27
|
+
// Return computed value if it exists
|
|
28
|
+
if (computed[property]) {
|
|
29
|
+
return computed[property]();
|
|
30
|
+
}
|
|
31
|
+
return target[property];
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
set(target, property, value) {
|
|
35
|
+
const oldValue = target[property];
|
|
36
|
+
|
|
37
|
+
// Only update if value actually changed
|
|
38
|
+
if (oldValue === value) return true;
|
|
39
|
+
|
|
40
|
+
// Update the value
|
|
41
|
+
target[property] = value;
|
|
42
|
+
|
|
43
|
+
// Call property-specific watchers
|
|
44
|
+
if (watchers[property]) {
|
|
45
|
+
watchers[property].forEach(watcher => {
|
|
46
|
+
watcher(value, oldValue);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Call global onChange callback
|
|
51
|
+
if (onChange) {
|
|
52
|
+
onChange(property, value, oldValue);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Add utility methods
|
|
60
|
+
proxy.$watch = function(property, callback) {
|
|
61
|
+
if (!watchers[property]) {
|
|
62
|
+
watchers[property] = [];
|
|
63
|
+
}
|
|
64
|
+
watchers[property].push(callback);
|
|
65
|
+
|
|
66
|
+
// Return unwatch function
|
|
67
|
+
return () => {
|
|
68
|
+
const index = watchers[property].indexOf(callback);
|
|
69
|
+
if (index > -1) {
|
|
70
|
+
watchers[property].splice(index, 1);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
proxy.$computed = function(property, computeFn) {
|
|
76
|
+
computed[property] = computeFn.bind(proxy);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
proxy.$reset = function(newState = initialState) {
|
|
80
|
+
Object.keys(state).forEach(key => delete state[key]);
|
|
81
|
+
Object.assign(state, newState);
|
|
82
|
+
if (onChange) onChange('$reset', newState);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
proxy.$raw = function() {
|
|
86
|
+
return { ...state };
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return proxy;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Create a global store (shared state across components)
|
|
94
|
+
*/
|
|
95
|
+
export function createStore(initialState = {}) {
|
|
96
|
+
const listeners = [];
|
|
97
|
+
const state = createState(initialState, (prop, value) => {
|
|
98
|
+
listeners.forEach(listener => listener(prop, value, state));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
state,
|
|
103
|
+
subscribe(callback) {
|
|
104
|
+
listeners.push(callback);
|
|
105
|
+
return () => {
|
|
106
|
+
const index = listeners.indexOf(callback);
|
|
107
|
+
if (index > -1) listeners.splice(index, 1);
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
getState() {
|
|
111
|
+
return state.$raw();
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HUMANJS FRAMEWORK
|
|
3
|
+
*
|
|
4
|
+
* A human-first frontend framework.
|
|
5
|
+
* Simple, readable, and easy to extend.
|
|
6
|
+
*
|
|
7
|
+
* @version 1.0.0
|
|
8
|
+
* @author HumanJS Team
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Core modules
|
|
12
|
+
export { createState, createStore } from './core/state.js';
|
|
13
|
+
export { html, createElement, fragment, when, each } from './core/render.js';
|
|
14
|
+
export { createComponent, app } from './core/component.js';
|
|
15
|
+
export { createRouter, Link, getParams, buildUrl } from './core/router.js';
|
|
16
|
+
export { attachEvents, delegate, debounce, throttle, on, mergeEvents } from './core/events.js';
|
|
17
|
+
|
|
18
|
+
// Plugins
|
|
19
|
+
export { createValidator, rules, getFormData, displayErrors } from './plugins/validator.js';
|
|
20
|
+
export { createHttp, http } from './plugins/http.js';
|
|
21
|
+
export { local, session, createNamespace, onStorageChange } from './plugins/storage.js';
|
|
22
|
+
|
|
23
|
+
// Utilities
|
|
24
|
+
export * as helpers from './utils/helpers.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Framework version
|
|
28
|
+
*/
|
|
29
|
+
export const VERSION = '1.0.0';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Quick start helper
|
|
33
|
+
*/
|
|
34
|
+
export function humanjs() {
|
|
35
|
+
console.log(`
|
|
36
|
+
🎯 HumanJS Framework v${VERSION}
|
|
37
|
+
|
|
38
|
+
A framework built for humans, not machines.
|
|
39
|
+
|
|
40
|
+
Quick start:
|
|
41
|
+
import { app, html } from './src/index.js';
|
|
42
|
+
|
|
43
|
+
app.create({
|
|
44
|
+
state: { count: 0 },
|
|
45
|
+
render: (state) => html\`<div>\${state.count}</div>\`
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
Documentation: https://github.com/kaiserofthenight/humanjs
|
|
49
|
+
`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Global error handler (optional)
|
|
54
|
+
*/
|
|
55
|
+
if (typeof window !== 'undefined') {
|
|
56
|
+
window.addEventListener('error', (event) => {
|
|
57
|
+
console.error('[HumanJS] Uncaught error:', event.error);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
61
|
+
console.error('[HumanJS] Unhandled promise rejection:', event.reason);
|
|
62
|
+
});
|
|
63
|
+
}
|