vaderjs 1.0.0-rv
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/READEME.MD +54 -0
- package/components/header.html +20 -0
- package/jsconfig.json +9 -0
- package/package.json +26 -0
- package/vader.js +867 -0
- package/vaderRouter.js +370 -0
package/READEME.MD
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# VaderJS: A Reactive Framework for Single-Page Applications (SPA)
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Postr-Inc/Vader.js/blob/main/LICENSE) [](https://www.npmjs.com/package/vaderjs)
|
|
4
|
+
|
|
5
|
+
VaderJS is a powerful and innovative reactive framework designed to simplify the development of Single-Page Applications (SPAs). Built with inspiration from React.js, VaderJS empowers developers to create dynamic and interactive web applications by leveraging a collection of functions and utilities tailored to the SPA paradigm. This overview provides insights into the core features and functionalities of VaderJS based on the discussions and code snippets shared in this conversation.
|
|
6
|
+
|
|
7
|
+
## Key Features
|
|
8
|
+
|
|
9
|
+
### Declarative Routing
|
|
10
|
+
|
|
11
|
+
VaderJS offers a declarative routing system that simplifies the navigation within your SPA. The `vaderRouter` class provides an intuitive interface for defining routes, handling errors, and managing URL parameters. By using the `start`, `get`, `use`, and `on` methods, developers can effortlessly create routes and associate them with corresponding components, enhancing the user experience through smooth navigation.
|
|
12
|
+
|
|
13
|
+
### State Management
|
|
14
|
+
|
|
15
|
+
The framework includes a versatile state management solution with functions like `useState`, allowing developers to efficiently manage and update the application's state. This approach ensures that your components remain responsive to changes and user interactions. Additionally, the `useSyncStore` and `useExternalStore` hooks facilitate synchronization and management of state across different components and interactions.
|
|
16
|
+
|
|
17
|
+
### Function Binding
|
|
18
|
+
|
|
19
|
+
VaderJS introduces the `registerFunction` utility, enabling seamless binding of JavaScript functions to vhtml elements within your components. This feature enhances code organization and promotes reusability by enabling developers to create functions that interact directly with the component's scope.
|
|
20
|
+
|
|
21
|
+
### Authentication and Authorization
|
|
22
|
+
|
|
23
|
+
With the `useAuth` function, VaderJS provides a comprehensive authentication and authorization system. Developers can define rulesets, roles, and conditions to control user access to specific actions and components. This feature ensures that your application remains secure and grants the appropriate level of access to authorized users.
|
|
24
|
+
|
|
25
|
+
### Global State Management
|
|
26
|
+
|
|
27
|
+
The framework includes the `createStore` function, which establishes a global state management solution. This centralized store allows components to subscribe to state changes and maintain synchronization throughout the application. The `useSyncStore` and `useExternalStore` hooks provide further options for accessing and manipulating global state.
|
|
28
|
+
|
|
29
|
+
### Simplified Component Creation
|
|
30
|
+
|
|
31
|
+
VaderJS streamlines the process of creating components with the `createComponent` function. This utility enables developers to define component functions and props, facilitating the creation of reusable and modular UI elements.
|
|
32
|
+
|
|
33
|
+
## Get Started with VaderJS
|
|
34
|
+
|
|
35
|
+
To start using VaderJS in your SPA project, follow these steps:
|
|
36
|
+
|
|
37
|
+
1. Install VaderJS from your preferred package manager:
|
|
38
|
+
```sh
|
|
39
|
+
npm install vaderjs
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
2. Import the necessary components and utilities into your project files.
|
|
43
|
+
|
|
44
|
+
3. Leverage the provided classes, functions, and hooks to implement routing, state management, function binding, authentication, and more.
|
|
45
|
+
|
|
46
|
+
4. Utilize the declarative syntax and intuitive APIs to create dynamic and responsive SPAs with enhanced user experiences.
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
VaderJS is released under the MIT License. See the [LICENSE](https://github.com/Postr-Inc/Vader.js/blob/main/LICENSE) file for more details.
|
|
51
|
+
|
|
52
|
+
## Join the VaderJS Community
|
|
53
|
+
|
|
54
|
+
Stay connected with the VaderJS community by following our [GitHub repository](https://github.com/Postr-Inc/Vader.js) and engaging in discussions, submitting issues, and contributing to the project's development. We welcome your feedback and contributions to make VaderJS even better for SPA development.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<header>
|
|
2
|
+
<div>
|
|
3
|
+
<h1>Header</h1>
|
|
4
|
+
<test
|
|
5
|
+
${props.styles}
|
|
6
|
+
>${props.color}</test>
|
|
7
|
+
|
|
8
|
+
${
|
|
9
|
+
function(){
|
|
10
|
+
|
|
11
|
+
if(props.color === 'red'){
|
|
12
|
+
return `<h1>Red</h1>`
|
|
13
|
+
}else{
|
|
14
|
+
return `<h1>Not Red</h1>`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
}()
|
|
18
|
+
}
|
|
19
|
+
</div>
|
|
20
|
+
</header>
|
package/jsconfig.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vaderjs",
|
|
3
|
+
"version": "1.0.0-rv",
|
|
4
|
+
"description": "A Reactive Framework for Single-Page Applications (SPA)",
|
|
5
|
+
"main": "vader.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/Postr-Inc/Vader.js.git"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"Reactjs",
|
|
15
|
+
"Vuejs",
|
|
16
|
+
"Reduxjs.",
|
|
17
|
+
"Nextjs",
|
|
18
|
+
"Vaderjs"
|
|
19
|
+
],
|
|
20
|
+
"author": "Malikwhitten67",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/Postr-Inc/Vader.js/issues"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/Postr-Inc/Vader.js#readme"
|
|
26
|
+
}
|
package/vader.js
ADDED
|
@@ -0,0 +1,867 @@
|
|
|
1
|
+
const templates = [];
|
|
2
|
+
const cache = {};
|
|
3
|
+
/**
|
|
4
|
+
* @file Vader.js
|
|
5
|
+
* @version 1.0.0
|
|
6
|
+
* @license MIT
|
|
7
|
+
* @description A simple ReactLike - framework for building web applications.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
*
|
|
11
|
+
* @param {*} strings
|
|
12
|
+
* @param {...any} values
|
|
13
|
+
* @returns {string}
|
|
14
|
+
* @description Creates a template literal and returns it as a string.
|
|
15
|
+
*/
|
|
16
|
+
const vhtml = (strings, ...values) => {
|
|
17
|
+
let result = "";
|
|
18
|
+
for (let i = 0; i < strings.length; i++) {
|
|
19
|
+
result += strings[i];
|
|
20
|
+
if (i < values.length) {
|
|
21
|
+
result += values[i];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
let dom = new DOMParser().parseFromString(result, "text/html");
|
|
25
|
+
|
|
26
|
+
let eventTypes = [
|
|
27
|
+
"click",
|
|
28
|
+
"dblclick",
|
|
29
|
+
"mousedown",
|
|
30
|
+
"mouseup",
|
|
31
|
+
"mouseenter",
|
|
32
|
+
"mouseleave",
|
|
33
|
+
"mousemove",
|
|
34
|
+
"keydown",
|
|
35
|
+
"keyup",
|
|
36
|
+
"focus",
|
|
37
|
+
"blur",
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
dom.querySelectorAll("[data-on]").forEach((element) => {
|
|
41
|
+
eventTypes.forEach((eventType) => {
|
|
42
|
+
const attributeName = `data-on-${eventType}`;
|
|
43
|
+
if (element.hasAttribute(attributeName)) {
|
|
44
|
+
awaitElement(element.tagName).then((el) => {
|
|
45
|
+
el.addEventListener(eventType, () => {
|
|
46
|
+
const eventCode = el.getAttribute(attributeName);
|
|
47
|
+
let eventFunction = new Function(eventCode);
|
|
48
|
+
eventFunction();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
dom.querySelectorAll("[id]").forEach((element) => {
|
|
56
|
+
const id = element.getAttribute("id");
|
|
57
|
+
window[id] = element;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return dom.body.innerHTML;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
let init = false;
|
|
64
|
+
/**
|
|
65
|
+
* @param {string} selector
|
|
66
|
+
* @param {function} rt
|
|
67
|
+
* @returns {void}
|
|
68
|
+
* @description Registers a template to be rendered.
|
|
69
|
+
*/
|
|
70
|
+
const render = async (template) => {
|
|
71
|
+
if (!document.querySelector(template.selector)) {
|
|
72
|
+
throw new Error(`No element found with selector ${template.selector}`);
|
|
73
|
+
} else {
|
|
74
|
+
const content = await template.rt();
|
|
75
|
+
|
|
76
|
+
document.querySelector(template.selector).innerHTML = content;
|
|
77
|
+
window["currentRender"] = template;
|
|
78
|
+
if (!init) {
|
|
79
|
+
init = true;
|
|
80
|
+
hydrate();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
register: () => {
|
|
85
|
+
register(template);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* @alias form
|
|
91
|
+
*
|
|
92
|
+
* @param {*} config
|
|
93
|
+
* @returns form component
|
|
94
|
+
* @description Creates a form component based on the config object.
|
|
95
|
+
* @example
|
|
96
|
+
* const form = form({
|
|
97
|
+
* name: 'myForm',
|
|
98
|
+
* fields: {
|
|
99
|
+
* name: {
|
|
100
|
+
* value: 'John Doe',
|
|
101
|
+
* type: 'text',
|
|
102
|
+
* placeholder: 'Enter your name'
|
|
103
|
+
* },
|
|
104
|
+
* email: {
|
|
105
|
+
* value: ''
|
|
106
|
+
* }
|
|
107
|
+
* },
|
|
108
|
+
* onSubmit: (e) => {
|
|
109
|
+
* console.log(e)
|
|
110
|
+
* }
|
|
111
|
+
*
|
|
112
|
+
*
|
|
113
|
+
* })
|
|
114
|
+
*/
|
|
115
|
+
const form = (config) => {
|
|
116
|
+
const { name, fields, onSubmit, inputs, button, onReset, onChange, rules } =
|
|
117
|
+
config;
|
|
118
|
+
const formData = {};
|
|
119
|
+
|
|
120
|
+
const componentInstance = {
|
|
121
|
+
state: {},
|
|
122
|
+
props: {},
|
|
123
|
+
reset: () => {
|
|
124
|
+
for (const fieldName in fields) {
|
|
125
|
+
formData[fieldName] = fields[fieldName].value || "";
|
|
126
|
+
}
|
|
127
|
+
document.forms[name].reset();
|
|
128
|
+
},
|
|
129
|
+
componentDidMount: (form) => {
|
|
130
|
+
form.setAttribute("onsubmit", "return false;");
|
|
131
|
+
|
|
132
|
+
for (const fieldName in fields) {
|
|
133
|
+
formData[fieldName] = fields[fieldName].value || "";
|
|
134
|
+
const fieldElement = form[fieldName];
|
|
135
|
+
|
|
136
|
+
fieldElement.addEventListener("input", (event) => {
|
|
137
|
+
formData[fieldName] = event.target.value;
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (onSubmit) {
|
|
142
|
+
document.onsubmit = (ev) => {
|
|
143
|
+
if (ev.target.name === name) {
|
|
144
|
+
let event = formData;
|
|
145
|
+
|
|
146
|
+
event["reset"] = componentInstance.reset;
|
|
147
|
+
onSubmit(event);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
} else if (onReset) {
|
|
151
|
+
document.onreset = (ev) => {
|
|
152
|
+
if (ev.target.name === name) {
|
|
153
|
+
for (const fieldName in fields) {
|
|
154
|
+
formData[fieldName] = fields[fieldName].value || "";
|
|
155
|
+
}
|
|
156
|
+
onReset();
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
} else if (onChange) {
|
|
160
|
+
document.onchange = (ev) => {
|
|
161
|
+
if (formData[ev.target.name]) {
|
|
162
|
+
let event = formData;
|
|
163
|
+
|
|
164
|
+
event["reset"] = componentInstance.reset;
|
|
165
|
+
onChange(event);
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
render: async () => {
|
|
171
|
+
const formElement = document.createElement("form");
|
|
172
|
+
formElement.name = name;
|
|
173
|
+
|
|
174
|
+
for (const fieldName in fields) {
|
|
175
|
+
const fieldConfig = fields[fieldName];
|
|
176
|
+
const fieldElement = document.createElement("input");
|
|
177
|
+
fieldElement.name = fieldName;
|
|
178
|
+
fieldElement.value = fieldConfig.value || "";
|
|
179
|
+
fieldElement.type = fieldConfig.type || "text";
|
|
180
|
+
fieldElement.placeholder = fieldConfig.placeholder || "";
|
|
181
|
+
if (inputs && inputs[fieldName]) {
|
|
182
|
+
const fieldStyles = inputs[fieldName];
|
|
183
|
+
for (const key in fieldStyles) {
|
|
184
|
+
fieldElement.style[key] = fieldStyles[key];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (rules) {
|
|
189
|
+
Object.keys(rules).forEach((rule) => {
|
|
190
|
+
if (rule === fieldName) {
|
|
191
|
+
const rulesobj = rules[rule];
|
|
192
|
+
// set all attributes to fieldElement
|
|
193
|
+
for (const key in rulesobj) {
|
|
194
|
+
fieldElement.setAttribute(key, rulesobj[key]);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
document.oninput = (ev) => {
|
|
201
|
+
let fieldName = ev.target.name;
|
|
202
|
+
let fieldValue = ev.target.value;
|
|
203
|
+
formData[fieldName] = fieldValue;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
// Add more attributes or properties to the fieldElement if needed
|
|
207
|
+
formElement.appendChild(fieldElement);
|
|
208
|
+
}
|
|
209
|
+
const submitButton = document.createElement("button");
|
|
210
|
+
submitButton.type = "submit";
|
|
211
|
+
submitButton.textContent = button.text || "Submit";
|
|
212
|
+
if (button.styles) {
|
|
213
|
+
for (const key in button.styles) {
|
|
214
|
+
submitButton.style[key] = button.styles[key];
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
formElement.appendChild(submitButton);
|
|
218
|
+
|
|
219
|
+
// Call componentDidMount
|
|
220
|
+
componentInstance.componentDidMount(formElement);
|
|
221
|
+
|
|
222
|
+
return formElement.outerHTML;
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
window.currentRender = componentInstance;
|
|
226
|
+
|
|
227
|
+
return componentInstance;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Component Lifecycle Hooks
|
|
232
|
+
*/
|
|
233
|
+
const componentLifecycle = {
|
|
234
|
+
componentWillMount: [],
|
|
235
|
+
componentDidMount: [],
|
|
236
|
+
componentWillUnmount: [],
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Register a lifecycle hook for a component
|
|
241
|
+
* @param {string} hookName - Name of the lifecycle hook
|
|
242
|
+
* @param {Function} hookFunction - Function to be called at the lifecycle stage
|
|
243
|
+
*/
|
|
244
|
+
const registerLifecycleHook = (hookName, hookFunction) => {
|
|
245
|
+
componentLifecycle[hookName].push(hookFunction);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Call all registered lifecycle hooks for a component
|
|
250
|
+
* @param {string} hookName - Name of the lifecycle hook
|
|
251
|
+
* @param {Object} component - Component object
|
|
252
|
+
*/
|
|
253
|
+
const callLifecycleHooks = (hookName, component) => {
|
|
254
|
+
componentLifecycle[hookName].forEach((hookFunction) => {
|
|
255
|
+
hookFunction(component);
|
|
256
|
+
});
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Component wrapper function
|
|
261
|
+
* @param {Function} componentFn - Component function
|
|
262
|
+
* @param {Object} props - Component props
|
|
263
|
+
* @returns {Promise} - Promise with the component content
|
|
264
|
+
*/
|
|
265
|
+
const component = async (componentFn, props) => {
|
|
266
|
+
let isMounted = true;
|
|
267
|
+
|
|
268
|
+
const componentInstance = {
|
|
269
|
+
state: {},
|
|
270
|
+
props,
|
|
271
|
+
setState: (newState) => {
|
|
272
|
+
if (isMounted) {
|
|
273
|
+
componentInstance.state = { ...componentInstance.state, ...newState };
|
|
274
|
+
render(template); // Assuming 'template' is defined somewhere
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
// Call componentDidMount lifecycle hooks
|
|
280
|
+
callLifecycleHooks("componentWillMount", componentInstance);
|
|
281
|
+
|
|
282
|
+
const content = await componentFn(props);
|
|
283
|
+
|
|
284
|
+
// Call componentDidMount lifecycle hooks
|
|
285
|
+
callLifecycleHooks("componentDidMount", componentInstance);
|
|
286
|
+
|
|
287
|
+
const unmount = () => {
|
|
288
|
+
isMounted = false;
|
|
289
|
+
// Call componentWillUnmount lifecycle hooks
|
|
290
|
+
callLifecycleHooks("componentWillUnmount", componentInstance);
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
...content,
|
|
295
|
+
unmount,
|
|
296
|
+
};
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Create a new component instance
|
|
301
|
+
* @param {Function} componentFn - Component function
|
|
302
|
+
* @param {Object} props - Component props
|
|
303
|
+
* @returns {Promise<ComponentInstance>} - Promise with the component instance
|
|
304
|
+
* @typedef {Object} ComponentInstance - Component instance
|
|
305
|
+
* @property {Object} state - Component state
|
|
306
|
+
* @property {Object} props - Component props
|
|
307
|
+
* @property {Function} setState - Function to update the component state
|
|
308
|
+
*/
|
|
309
|
+
const createComponent = async (componentFn, props) => {
|
|
310
|
+
const componentInstance = {
|
|
311
|
+
state: {},
|
|
312
|
+
props,
|
|
313
|
+
setState: (newState) => {
|
|
314
|
+
componentInstance.state = { ...componentInstance.state, ...newState };
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
/** @type {Function} */
|
|
318
|
+
return await componentFn(props);
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// ... (other functions)
|
|
322
|
+
|
|
323
|
+
// Exported functions
|
|
324
|
+
export {
|
|
325
|
+
render,
|
|
326
|
+
vhtml,
|
|
327
|
+
component,
|
|
328
|
+
form,
|
|
329
|
+
useAuth,
|
|
330
|
+
createComponent,
|
|
331
|
+
useExternalStore,
|
|
332
|
+
useState,
|
|
333
|
+
useEffect,
|
|
334
|
+
useReduce,
|
|
335
|
+
useSyncStore,
|
|
336
|
+
require,
|
|
337
|
+
$s,
|
|
338
|
+
registerFunction,
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// ... (remaining code)
|
|
342
|
+
|
|
343
|
+
function hydrate() {
|
|
344
|
+
templates.forEach(async (template) => {
|
|
345
|
+
if (template === window["currentRender"]) {
|
|
346
|
+
render(template);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const content = await template.rt();
|
|
350
|
+
const element = document.querySelector(template.selector);
|
|
351
|
+
|
|
352
|
+
if (element) {
|
|
353
|
+
if (template.renderedContent !== content) {
|
|
354
|
+
element.innerHTML = content;
|
|
355
|
+
template.renderedContent = content;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Register a function to be available in the component scope for vhtml elements.
|
|
362
|
+
*
|
|
363
|
+
* @param {string} name - The name to assign to the function in the global scope.
|
|
364
|
+
* @param {Function} fn - The function to register.
|
|
365
|
+
* @throws {Error} Throws an error if the name is not a string or if the function is not a valid function.
|
|
366
|
+
* @throws {Error} Throws an error if the name is already used in the global scope.
|
|
367
|
+
* @param {string} name @type {string}
|
|
368
|
+
* @example
|
|
369
|
+
* function login() {
|
|
370
|
+
* setState({ loggedIn: true });
|
|
371
|
+
* }
|
|
372
|
+
* registerFunction('login', login);
|
|
373
|
+
* return html`<button onclick="login()">Login</button>`;
|
|
374
|
+
*/
|
|
375
|
+
const registerFunction = (name, fn) => {
|
|
376
|
+
if (typeof name !== "string") {
|
|
377
|
+
throw new Error("The name parameter must be a string.");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (typeof fn !== "function") {
|
|
381
|
+
throw new Error("The fn parameter must be a function.");
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* @global
|
|
388
|
+
* @function
|
|
389
|
+
* @name {Function}
|
|
390
|
+
*/
|
|
391
|
+
window[name] = fn;
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
function doxMethods(element) {
|
|
395
|
+
element.on = function (event, callback) {
|
|
396
|
+
element.addEventListener(event, callback);
|
|
397
|
+
};
|
|
398
|
+
element.query = function (selector) {
|
|
399
|
+
return element.querySelector(selector);
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
return element;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const awaitElement = (selector) => {
|
|
406
|
+
return new Promise((resolve, reject) => {
|
|
407
|
+
const interval = setInterval(() => {
|
|
408
|
+
if (document.querySelector(selector)) {
|
|
409
|
+
clearInterval(interval);
|
|
410
|
+
resolve(document.querySelector(selector));
|
|
411
|
+
}
|
|
412
|
+
}, 100);
|
|
413
|
+
});
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Create a new state variable with optional initial value.
|
|
418
|
+
*
|
|
419
|
+
* @param {string} StateName - The name of the state variable.
|
|
420
|
+
* @param {*} initialState - The initial state value.
|
|
421
|
+
* @returns {Array} An array containing the current state value and a function to update the state.
|
|
422
|
+
* @typedef {Array} StateHook
|
|
423
|
+
* @property {*} 0 - The current state value.
|
|
424
|
+
* @property {Function} 1 - A function to update the state.
|
|
425
|
+
*/
|
|
426
|
+
const useState = (StateName, initialState) => {
|
|
427
|
+
let currentstate;
|
|
428
|
+
|
|
429
|
+
// Attempt to retrieve state from sessionStorage
|
|
430
|
+
if (sessionStorage.getItem(StateName)) {
|
|
431
|
+
currentstate = JSON.parse(sessionStorage.getItem(StateName));
|
|
432
|
+
} else {
|
|
433
|
+
// If state is not found in sessionStorage, use initial state
|
|
434
|
+
currentstate = initialState;
|
|
435
|
+
sessionStorage.setItem(StateName, JSON.stringify(initialState));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Update the state with a new value.
|
|
440
|
+
*
|
|
441
|
+
* @param {*} newState - The new state value.
|
|
442
|
+
* @returns {*} The new state value.
|
|
443
|
+
*/
|
|
444
|
+
const setState = (newState) => {
|
|
445
|
+
currentstate = newState;
|
|
446
|
+
sessionStorage.setItem(StateName, JSON.stringify(newState));
|
|
447
|
+
let clonedState = JSON.parse(JSON.stringify(newState));
|
|
448
|
+
window.postMessage({ state: clonedState }, "*");
|
|
449
|
+
hydrate();
|
|
450
|
+
window[StateName] = newState;
|
|
451
|
+
return newState;
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
return [currentstate, setState];
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Update a state variable in sessionStorage and the global scope.
|
|
459
|
+
*
|
|
460
|
+
* @param {string} statename - The name of the state variable.
|
|
461
|
+
* @param {*} newState - The new state value.
|
|
462
|
+
*/
|
|
463
|
+
const setState = (statename, newState) => {
|
|
464
|
+
window[statename] = sessionStorage.setItem(
|
|
465
|
+
statename,
|
|
466
|
+
JSON.stringify(newState)
|
|
467
|
+
);
|
|
468
|
+
window.postMessage({ state: newState }, "*");
|
|
469
|
+
hydrate();
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Create a store with state management functionality.
|
|
474
|
+
*
|
|
475
|
+
* @param {Object} initialState - The initial state of the store.
|
|
476
|
+
* @returns {Object} An object containing state management functions.
|
|
477
|
+
*/
|
|
478
|
+
const createStore = (initialState) => {
|
|
479
|
+
let state = initialState;
|
|
480
|
+
const subscribers = new Set();
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Update the store's state and notify subscribers.
|
|
484
|
+
*
|
|
485
|
+
* @param {Object} newState - The new state to set.
|
|
486
|
+
*/
|
|
487
|
+
const setState = (newState) => {
|
|
488
|
+
state = newState;
|
|
489
|
+
subscribers.forEach((subscriber) => subscriber(state));
|
|
490
|
+
|
|
491
|
+
// Update local storage with the new state
|
|
492
|
+
if (localStorage.getItem("store")) {
|
|
493
|
+
let store = JSON.parse(localStorage.getItem("store"));
|
|
494
|
+
store = { ...store, ...state };
|
|
495
|
+
localStorage.setItem("store", JSON.stringify(store));
|
|
496
|
+
} else {
|
|
497
|
+
localStorage.setItem("store", JSON.stringify(state));
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Notify other windows/frames about the state change
|
|
501
|
+
window.postMessage({ state: state }, "*");
|
|
502
|
+
hydrate();
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Subscribe a function to be notified of state changes.
|
|
507
|
+
*
|
|
508
|
+
* @param {Function} subscriber - The function to call when the state changes.
|
|
509
|
+
* @returns {Function} A function to unsubscribe the subscriber.
|
|
510
|
+
*/
|
|
511
|
+
const subscribe = (subscriber) => {
|
|
512
|
+
subscribers.add(subscriber);
|
|
513
|
+
return () => {
|
|
514
|
+
subscribers.delete(subscriber);
|
|
515
|
+
};
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
return { state, setState, subscribe };
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Create an authentication object with utility methods for managing user permissions and roles.
|
|
523
|
+
*
|
|
524
|
+
* @param {Object} options - The options object.
|
|
525
|
+
* @param {Array} options.rulesets - An array of rulesets defining user permissions.
|
|
526
|
+
* @param {Object} options.user - The user object containing roles and other information.
|
|
527
|
+
* @returns {Object} The authentication object with utility methods.
|
|
528
|
+
*/
|
|
529
|
+
function useAuth(options) {
|
|
530
|
+
if (!options.rulesets) {
|
|
531
|
+
throw new Error("No rulesets provided");
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
let rules = options.rulesets;
|
|
535
|
+
let user = options.user;
|
|
536
|
+
|
|
537
|
+
const auth = {
|
|
538
|
+
/**
|
|
539
|
+
* Check if the user can perform a specific action.
|
|
540
|
+
*
|
|
541
|
+
* @param {string} action - The action to check.
|
|
542
|
+
* @returns {boolean} True if the user can perform the action, false otherwise.
|
|
543
|
+
*/
|
|
544
|
+
can: (action) => {
|
|
545
|
+
let can = false;
|
|
546
|
+
rules.forEach((rule) => {
|
|
547
|
+
if (rule.action === action) {
|
|
548
|
+
if (rule.condition(user)) {
|
|
549
|
+
can = true;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
return can;
|
|
554
|
+
},
|
|
555
|
+
/**
|
|
556
|
+
* Check if the user has a specific role.
|
|
557
|
+
*
|
|
558
|
+
* @param {string} role - The role to check.
|
|
559
|
+
* @returns {boolean} True if the user has the role, false otherwise.
|
|
560
|
+
*/
|
|
561
|
+
hasRole: (role) => {
|
|
562
|
+
return user.roles && user.roles.includes(role);
|
|
563
|
+
},
|
|
564
|
+
/**
|
|
565
|
+
* Check if the user can perform a specific action with a specific role.
|
|
566
|
+
*
|
|
567
|
+
* @param {string} action - The action to check.
|
|
568
|
+
* @param {string} role - The role to check.
|
|
569
|
+
* @returns {boolean} True if the user can perform the action with the role, false otherwise.
|
|
570
|
+
*/
|
|
571
|
+
canWithRole: (action, role) => {
|
|
572
|
+
return auth.can(action) && auth.hasRole(role);
|
|
573
|
+
},
|
|
574
|
+
/**
|
|
575
|
+
* Assign a new rule to the rulesets.
|
|
576
|
+
*
|
|
577
|
+
* @param {Object} rule - The rule to assign.
|
|
578
|
+
*/
|
|
579
|
+
assignRule: (rule) => {
|
|
580
|
+
if (!rules.some((existingRule) => existingRule.action === rule.action)) {
|
|
581
|
+
rules.push(rule);
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
/**
|
|
585
|
+
* Revoke a rule from the rulesets.
|
|
586
|
+
*
|
|
587
|
+
* @param {string} action - The action of the rule to revoke.
|
|
588
|
+
*/
|
|
589
|
+
revokeRule: (action) => {
|
|
590
|
+
rules = rules.filter((rule) => rule.action !== action);
|
|
591
|
+
},
|
|
592
|
+
/**
|
|
593
|
+
* Check if the user can perform any of the specified actions.
|
|
594
|
+
*
|
|
595
|
+
* @param {Array} actions - An array of actions to check.
|
|
596
|
+
* @returns {boolean} True if the user can perform any of the actions, false otherwise.
|
|
597
|
+
*/
|
|
598
|
+
canAnyOf: (actions) => {
|
|
599
|
+
return actions.some((action) => auth.can(action));
|
|
600
|
+
},
|
|
601
|
+
/**
|
|
602
|
+
* Check if the user can perform all of the specified actions.
|
|
603
|
+
*
|
|
604
|
+
* @param {Array} actions - An array of actions to check.
|
|
605
|
+
* @returns {boolean} True if the user can perform all of the actions, false otherwise.
|
|
606
|
+
*/
|
|
607
|
+
canAllOf: (actions) => {
|
|
608
|
+
return actions.every((action) => auth.can(action));
|
|
609
|
+
},
|
|
610
|
+
/**
|
|
611
|
+
* Check if the user can perform a group of actions based on a logical operator.
|
|
612
|
+
*
|
|
613
|
+
* @param {Array} actions - An array of actions to check.
|
|
614
|
+
* @param {string} logicalOperator - The logical operator to use ('any' or 'all').
|
|
615
|
+
* @returns {boolean} True if the user can perform the actions based on the logical operator, false otherwise.
|
|
616
|
+
*/
|
|
617
|
+
canGroup: (actions, logicalOperator = "any") => {
|
|
618
|
+
return logicalOperator === "any"
|
|
619
|
+
? auth.canAnyOf(actions)
|
|
620
|
+
: auth.canAllOf(actions);
|
|
621
|
+
},
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
return auth;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Create a synchronized store with field-level state management.
|
|
629
|
+
*
|
|
630
|
+
* @param {string} storeName - The name of the store.
|
|
631
|
+
* @param {Object} initialState - The initial state of the store.
|
|
632
|
+
* @returns {Object} An object containing field management functions.
|
|
633
|
+
*/
|
|
634
|
+
const useSyncStore = (storeName, initialState) => {
|
|
635
|
+
const storedState = JSON.parse(localStorage.getItem("store")) || initialState;
|
|
636
|
+
const store = createStore(storedState);
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Get the value of a specific field from the store's state.
|
|
640
|
+
*
|
|
641
|
+
* @param {string} fieldName - The name of the field.
|
|
642
|
+
* @returns {*} The value of the specified field.
|
|
643
|
+
*/
|
|
644
|
+
const getField = (fieldName) => {
|
|
645
|
+
return store.state[fieldName];
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Set the value of a specific field in the store's state.
|
|
650
|
+
*
|
|
651
|
+
* @param {string} fieldName - The name of the field.
|
|
652
|
+
* @param {*} value - The new value to set for the field.
|
|
653
|
+
*/
|
|
654
|
+
const setField = (fieldName, value) => {
|
|
655
|
+
const newState = { ...store.state, [fieldName]: value };
|
|
656
|
+
store.setState(newState);
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Subscribe a function to be notified of state changes.
|
|
661
|
+
*
|
|
662
|
+
* @param {Function} subscriber - The function to call when the state changes.
|
|
663
|
+
* @returns {Function} A function to unsubscribe the subscriber.
|
|
664
|
+
*/
|
|
665
|
+
const subscribe = (subscriber) => {
|
|
666
|
+
return store.subscribe(subscriber);
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Clear the stored state from local storage.
|
|
671
|
+
*/
|
|
672
|
+
const clear = () => {
|
|
673
|
+
localStorage.setItem("store", "");
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
return {
|
|
677
|
+
getField,
|
|
678
|
+
setField,
|
|
679
|
+
subscribe,
|
|
680
|
+
clear,
|
|
681
|
+
};
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Create an external store with API integration and local storage caching.
|
|
686
|
+
*
|
|
687
|
+
* @param {Object} options - Configuration options for the external store.
|
|
688
|
+
* @param {Function} options.api - The API method to fetch data.
|
|
689
|
+
* @param {string} options.storeKey - The key for storing data in local storage.
|
|
690
|
+
* @param {Object} initialState - The initial state of the store.
|
|
691
|
+
* @returns {Object} An object containing store management functions.
|
|
692
|
+
*/
|
|
693
|
+
const useExternalStore = (options, initialState) => {
|
|
694
|
+
const store = createStore(initialState);
|
|
695
|
+
const { subscribe, setState } = store;
|
|
696
|
+
|
|
697
|
+
let consecutiveFetches = 0;
|
|
698
|
+
let lastFetchedData = null;
|
|
699
|
+
let debounceTimer = null;
|
|
700
|
+
|
|
701
|
+
if (options.api) {
|
|
702
|
+
const apiMethod = options.api;
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Update the store with fetched data from the API.
|
|
706
|
+
*
|
|
707
|
+
* @param {...*} args - Arguments to pass to the API method.
|
|
708
|
+
*/
|
|
709
|
+
store.update = async (...args) => {
|
|
710
|
+
try {
|
|
711
|
+
const fetchedData = await apiMethod(...args);
|
|
712
|
+
|
|
713
|
+
if (JSON.stringify(fetchedData) === JSON.stringify(lastFetchedData)) {
|
|
714
|
+
consecutiveFetches++;
|
|
715
|
+
} else {
|
|
716
|
+
consecutiveFetches = 1;
|
|
717
|
+
lastFetchedData = fetchedData;
|
|
718
|
+
|
|
719
|
+
// Save the fetched data to localStorage
|
|
720
|
+
localStorage.setItem(options.storeKey, JSON.stringify(fetchedData));
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
if (consecutiveFetches === 3) {
|
|
724
|
+
clearTimeout(debounceTimer); // Clear previous timer
|
|
725
|
+
debounceTimer = setTimeout(() => {
|
|
726
|
+
console.log("Updating state with fetched data:", fetchedData);
|
|
727
|
+
setState(fetchedData);
|
|
728
|
+
consecutiveFetches = 0;
|
|
729
|
+
}, 500); // Adjust the delay time as needed
|
|
730
|
+
}
|
|
731
|
+
} catch (error) {
|
|
732
|
+
console.error("Error fetching data:", error);
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
// Load initial state from localStorage
|
|
737
|
+
const storedData = localStorage.getItem(options.storeKey);
|
|
738
|
+
if (storedData) {
|
|
739
|
+
localStorage.setItem(options.storeKey, storedData);
|
|
740
|
+
if (!isntHydrated) {
|
|
741
|
+
hydrate(isntHydrated);
|
|
742
|
+
isntHydrated = true;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
return {
|
|
748
|
+
...store,
|
|
749
|
+
subscribe,
|
|
750
|
+
delete: () => {
|
|
751
|
+
localStorage.removeItem(options.storeKey);
|
|
752
|
+
},
|
|
753
|
+
clear: () => {
|
|
754
|
+
localStorage.setItem(options.storeKey, "");
|
|
755
|
+
},
|
|
756
|
+
return: () => {
|
|
757
|
+
isntHydrated = false;
|
|
758
|
+
},
|
|
759
|
+
};
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Create a state management hook that uses a reducer function to update state.
|
|
764
|
+
*
|
|
765
|
+
* @template S, A
|
|
766
|
+
* @param {function(S, A): S} reducer - The reducer function that updates the state.
|
|
767
|
+
* @param {S} initialState - The initial state value.
|
|
768
|
+
* @returns {[S, function(A): void]} An array containing the current state and a dispatch function.
|
|
769
|
+
*/
|
|
770
|
+
const useReduce = (reducer, initialState) => {
|
|
771
|
+
/**
|
|
772
|
+
* The current state managed by the reducer.
|
|
773
|
+
* @type {S}
|
|
774
|
+
*/
|
|
775
|
+
const [state, setState] = useState(initialState + "state", initialState);
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Dispatches an action to the reducer to update the state.
|
|
779
|
+
*
|
|
780
|
+
* @param {A} action - The action to be dispatched.
|
|
781
|
+
*/
|
|
782
|
+
const dispatch = (action) => {
|
|
783
|
+
const newState = reducer(state, action);
|
|
784
|
+
setState(newState);
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
return [state, dispatch];
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
const getState = (statename) => {
|
|
791
|
+
const storedState = sessionStorage.getItem(statename);
|
|
792
|
+
return storedState ? JSON.parse(storedState) : null;
|
|
793
|
+
};
|
|
794
|
+
window["getState"] = getState;
|
|
795
|
+
window["setState"] = setState;
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
*
|
|
799
|
+
* @param {Function} callback
|
|
800
|
+
* @param {Array} dependencies
|
|
801
|
+
* @example
|
|
802
|
+
* useEffect(() => {
|
|
803
|
+
* console.log('state changed');
|
|
804
|
+
* }, ['state']);
|
|
805
|
+
*/
|
|
806
|
+
const useEffect = (callback, dependencies) => {
|
|
807
|
+
callback();
|
|
808
|
+
window.addEventListener("message", (event) => {
|
|
809
|
+
if (dependencies.includes(event.data.state)) {
|
|
810
|
+
callback(event.data.state);
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
const register = (template) => {
|
|
817
|
+
templates.push(template);
|
|
818
|
+
};
|
|
819
|
+
/**
|
|
820
|
+
*
|
|
821
|
+
* @param {string} path
|
|
822
|
+
* @param {object} props
|
|
823
|
+
* @returns {Promise} - Promise with the obstructed html.
|
|
824
|
+
* @example
|
|
825
|
+
* const header = await require('./components/header.html');
|
|
826
|
+
*/
|
|
827
|
+
|
|
828
|
+
const require = (path, props) => {
|
|
829
|
+
const promise = new Promise((resolve, reject) => {
|
|
830
|
+
if (cache[path]) {
|
|
831
|
+
resolve(cache[path]);
|
|
832
|
+
}
|
|
833
|
+
if (!path.endsWith(".html")) {
|
|
834
|
+
reject("Only html files are supported");
|
|
835
|
+
}
|
|
836
|
+
fetch(path)
|
|
837
|
+
.then((response) => {
|
|
838
|
+
return response.text();
|
|
839
|
+
})
|
|
840
|
+
.then((code) => {
|
|
841
|
+
cache[path] = new Function("props", "return " + "`" + code + "`");
|
|
842
|
+
hydrate();
|
|
843
|
+
resolve(cache[path]);
|
|
844
|
+
});
|
|
845
|
+
});
|
|
846
|
+
return promise;
|
|
847
|
+
};
|
|
848
|
+
window["require"] = require;
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* @typedef {Object} $s
|
|
852
|
+
* @property {object} styles - Object with css properties.
|
|
853
|
+
* @returns {string} - String with css properties.
|
|
854
|
+
* @example
|
|
855
|
+
* $s({ color: 'red' })
|
|
856
|
+
*/
|
|
857
|
+
function $s(styles = {}) {
|
|
858
|
+
let result = "";
|
|
859
|
+
for (let key in styles) {
|
|
860
|
+
result += `${key}: ${styles[key]};`;
|
|
861
|
+
}
|
|
862
|
+
return `style="${result}"`;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
window.onbeforeunload = function () {
|
|
866
|
+
sessionStorage.clear();
|
|
867
|
+
};
|
package/vaderRouter.js
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file VaderRouter.js
|
|
3
|
+
* @version 1.0.0
|
|
4
|
+
* @license MIT
|
|
5
|
+
* @description A simple router for single page applications.
|
|
6
|
+
*/
|
|
7
|
+
class VaderRouter {
|
|
8
|
+
/**
|
|
9
|
+
* Creates an instance of VaderRouter.
|
|
10
|
+
* @param {string} starturl - The starting URL for the router.
|
|
11
|
+
*/
|
|
12
|
+
constructor(starturl) {
|
|
13
|
+
/**
|
|
14
|
+
* Object to store route information.
|
|
15
|
+
* @type {Object}
|
|
16
|
+
*/
|
|
17
|
+
this.routes = {};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The current URL being navigated.
|
|
21
|
+
* @type {string}
|
|
22
|
+
*/
|
|
23
|
+
this.currentUrl = "";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Listener function for hash change events.
|
|
27
|
+
* @type {Function}
|
|
28
|
+
*/
|
|
29
|
+
this.hashChangeListener = null;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Error handlers for different error types.
|
|
33
|
+
* @type {Object}
|
|
34
|
+
*/
|
|
35
|
+
this.errorHandlers = {};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Flag indicating if custom error handling is enabled.
|
|
39
|
+
* @type {boolean}
|
|
40
|
+
*/
|
|
41
|
+
this.customerror = null;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Flag indicating if the router is currently handling a route.
|
|
45
|
+
* @type {boolean}
|
|
46
|
+
*/
|
|
47
|
+
this.hooked = false;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The starting URL for the router.
|
|
51
|
+
* @type {string}
|
|
52
|
+
*/
|
|
53
|
+
this.starturl = starturl;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Array to store stored routes.
|
|
57
|
+
* @type {string[]}
|
|
58
|
+
*/
|
|
59
|
+
this.storedroutes = [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Starts the router.
|
|
64
|
+
*/
|
|
65
|
+
start() {
|
|
66
|
+
if (!this.routes[window.location.hash.substring(1)]) {
|
|
67
|
+
window.location.hash = this.starturl;
|
|
68
|
+
}
|
|
69
|
+
window.addEventListener("hashchange", () => {
|
|
70
|
+
let hash = window.location.hash.substring(1).split("/")
|
|
71
|
+
? window.location.hash.substring(1).split("/")
|
|
72
|
+
: window.location.hash.substring(1);
|
|
73
|
+
// remove '' from array
|
|
74
|
+
hash = hash.filter((item) => item !== "");
|
|
75
|
+
const basePath = "/" + hash[0];
|
|
76
|
+
|
|
77
|
+
if (!this.routes[basePath] && !this.customerror) {
|
|
78
|
+
window.location.hash = this.starturl;
|
|
79
|
+
} else if (!this.routes[basePath] && this.customerror) {
|
|
80
|
+
const errBody = {
|
|
81
|
+
status: 404,
|
|
82
|
+
message: "Page not found",
|
|
83
|
+
};
|
|
84
|
+
this.handleError("404", errBody);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @alias handleErrors
|
|
91
|
+
* @param {*} type
|
|
92
|
+
* @param {*} callback
|
|
93
|
+
* @returns {void}
|
|
94
|
+
* @memberof VaderRouter
|
|
95
|
+
* @description Handles errors for the router.
|
|
96
|
+
* @example
|
|
97
|
+
* router.handleErrors('404', (err) => {
|
|
98
|
+
* // do something with err
|
|
99
|
+
* });
|
|
100
|
+
*/
|
|
101
|
+
handleErrors(type, callback) {
|
|
102
|
+
this.errorHandlers[type] = callback;
|
|
103
|
+
this.customerror = true;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
*
|
|
107
|
+
* @param {*} type
|
|
108
|
+
* @param {*} data
|
|
109
|
+
* @returns {void}
|
|
110
|
+
* @memberof VaderRouter
|
|
111
|
+
* @description used by start() to handle errors.
|
|
112
|
+
*/
|
|
113
|
+
|
|
114
|
+
handleError(type, data) {
|
|
115
|
+
if (this.errorHandlers[type]) {
|
|
116
|
+
this.errorHandlers[type](data);
|
|
117
|
+
} else {
|
|
118
|
+
console.error(`No error handler found for type: ${type}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* @alias get
|
|
123
|
+
* @param {*} path
|
|
124
|
+
* @param {*} callback
|
|
125
|
+
* @returns {void}
|
|
126
|
+
* @memberof VaderRouter
|
|
127
|
+
* @description Allows you to perform actions when path matches the current Route on visit.
|
|
128
|
+
*/
|
|
129
|
+
get(path, callback) {
|
|
130
|
+
const paramNames = [];
|
|
131
|
+
const queryNames = [];
|
|
132
|
+
const parsedPath = path
|
|
133
|
+
.split("/")
|
|
134
|
+
.map((part) => {
|
|
135
|
+
if (part.startsWith(":")) {
|
|
136
|
+
paramNames.push(part.substring(1));
|
|
137
|
+
return "([^/]+)";
|
|
138
|
+
}
|
|
139
|
+
if (part.startsWith("*")) {
|
|
140
|
+
paramNames.push(part.substring(1));
|
|
141
|
+
return "(.*)";
|
|
142
|
+
}
|
|
143
|
+
if (part.startsWith("?")) {
|
|
144
|
+
queryNames.push(part.substring(1));
|
|
145
|
+
return "([^/]+)";
|
|
146
|
+
}
|
|
147
|
+
return part;
|
|
148
|
+
})
|
|
149
|
+
.join("/");
|
|
150
|
+
const regex = new RegExp("^" + parsedPath + "(\\?(.*))?$");
|
|
151
|
+
|
|
152
|
+
if (window.location.hash.substring(1).match(regex)) {
|
|
153
|
+
this.storedroutes.push(window.location.hash.substring(1));
|
|
154
|
+
const matches = window.location.hash.substring(1).match(regex);
|
|
155
|
+
const params = {};
|
|
156
|
+
|
|
157
|
+
for (let i = 0; i < paramNames.length; i++) {
|
|
158
|
+
params[paramNames[i]] = matches[i + 1];
|
|
159
|
+
}
|
|
160
|
+
if (
|
|
161
|
+
path.includes(":") &&
|
|
162
|
+
window.location.hash.substring(1).split("?")[1]
|
|
163
|
+
) {
|
|
164
|
+
if (debug.enabled) {
|
|
165
|
+
debug.log(
|
|
166
|
+
[
|
|
167
|
+
`
|
|
168
|
+
Cannot use query params with path params ${path} ${
|
|
169
|
+
window.location.hash.substring(1).split("?")[1]
|
|
170
|
+
}`,
|
|
171
|
+
],
|
|
172
|
+
"assert"
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
const query = {};
|
|
179
|
+
|
|
180
|
+
const queryString = window.location.hash.substring(1).split("?")[1];
|
|
181
|
+
if (queryString) {
|
|
182
|
+
const queryParts = queryString.split("&");
|
|
183
|
+
for (let i = 0; i < queryParts.length; i++) {
|
|
184
|
+
const queryParam = queryParts[i].split("=");
|
|
185
|
+
query[queryParam[0]] = queryParam[1];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* @alias req
|
|
190
|
+
* @type {Object}
|
|
191
|
+
* @property {Object} params - The params object.
|
|
192
|
+
* @returns {Object} current url params
|
|
193
|
+
*/
|
|
194
|
+
|
|
195
|
+
const req = {
|
|
196
|
+
params: params,
|
|
197
|
+
query: query,
|
|
198
|
+
url: window.location.hash.substring(1),
|
|
199
|
+
method: "GET",
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
window.$URL_PARAMS = params;
|
|
203
|
+
window.$URL_QUERY = query;
|
|
204
|
+
|
|
205
|
+
const res = {
|
|
206
|
+
return: function (data) {
|
|
207
|
+
this.hooked = false;
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
callback(req, res);
|
|
212
|
+
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
this.hooked = false;
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
kill(path) {
|
|
221
|
+
const listener = this.listeners[path];
|
|
222
|
+
|
|
223
|
+
if (listener) {
|
|
224
|
+
window.removeEventListener("message", listener);
|
|
225
|
+
delete this.listeners[path];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* @alias use
|
|
230
|
+
* @param {*} path
|
|
231
|
+
* @returns {void}
|
|
232
|
+
* @memberof VaderRouter
|
|
233
|
+
* @description Allows you to set routes to be used throughout your spa.
|
|
234
|
+
*/
|
|
235
|
+
use(path) {
|
|
236
|
+
const paramNames = [];
|
|
237
|
+
const queryNames = [];
|
|
238
|
+
const parsedPath = path
|
|
239
|
+
.split("/")
|
|
240
|
+
.map((part) => {
|
|
241
|
+
if (part.startsWith(":")) {
|
|
242
|
+
paramNames.push(part.substring(1));
|
|
243
|
+
return "([^/]+)";
|
|
244
|
+
}
|
|
245
|
+
if (part.startsWith("*")) {
|
|
246
|
+
paramNames.push(part.substring(1));
|
|
247
|
+
return "(.*)";
|
|
248
|
+
}
|
|
249
|
+
if (part.startsWith("?")) {
|
|
250
|
+
queryNames.push(part.substring(1));
|
|
251
|
+
return "([^/]+)";
|
|
252
|
+
}
|
|
253
|
+
return part;
|
|
254
|
+
})
|
|
255
|
+
.join("/");
|
|
256
|
+
const regex = new RegExp("^" + parsedPath + "(\\?(.*))?$");
|
|
257
|
+
path = parsedPath;
|
|
258
|
+
this.routes[path] = true;
|
|
259
|
+
this.storedroutes.push(path);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
onload(callback) {
|
|
263
|
+
// await dom to be done make sure no new elements are added
|
|
264
|
+
if (
|
|
265
|
+
document.readyState === "complete" ||
|
|
266
|
+
document.readyState === "loaded" ||
|
|
267
|
+
document.readyState === "interactive"
|
|
268
|
+
) {
|
|
269
|
+
callback();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* @alias on
|
|
275
|
+
* @param {*} path
|
|
276
|
+
* @param {*} callback
|
|
277
|
+
* @returns {void}
|
|
278
|
+
* @memberof VaderRouter
|
|
279
|
+
* @description Allows you to perform actions when the currentRoute changes.
|
|
280
|
+
*/
|
|
281
|
+
on(path, callback) {
|
|
282
|
+
window.addEventListener("hashchange", () => {
|
|
283
|
+
const paramNames = [];
|
|
284
|
+
const queryNames = [];
|
|
285
|
+
const parsedPath = path
|
|
286
|
+
.split("/")
|
|
287
|
+
.map((part) => {
|
|
288
|
+
if (part.startsWith(":")) {
|
|
289
|
+
paramNames.push(part.substring(1));
|
|
290
|
+
return "([^/]+)";
|
|
291
|
+
}
|
|
292
|
+
if (part.startsWith("*")) {
|
|
293
|
+
paramNames.push(part.substring(1));
|
|
294
|
+
return "(.*)";
|
|
295
|
+
}
|
|
296
|
+
if (part.startsWith("?")) {
|
|
297
|
+
queryNames.push(part.substring(1));
|
|
298
|
+
return "([^/]+)";
|
|
299
|
+
}
|
|
300
|
+
return part;
|
|
301
|
+
})
|
|
302
|
+
.join("/");
|
|
303
|
+
const regex = new RegExp("^" + parsedPath + "(\\?(.*))?$");
|
|
304
|
+
|
|
305
|
+
this.currentUrl = path;
|
|
306
|
+
// replace params if preset
|
|
307
|
+
let route = "";
|
|
308
|
+
if (path.includes(":")) {
|
|
309
|
+
route = path.split(":")[0].replace(/\/$/, "");
|
|
310
|
+
} else {
|
|
311
|
+
route = path.replace(/\/$/, "");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
window.$CURRENT_URL = route;
|
|
315
|
+
window.$URL_PARAMS = {};
|
|
316
|
+
if (
|
|
317
|
+
window.location.hash.substring(1).match(regex) &&
|
|
318
|
+
this.routes[$CURRENT_URL]
|
|
319
|
+
) {
|
|
320
|
+
this.storedroutes.push(window.location.hash.substring(1));
|
|
321
|
+
const matches = window.location.hash.substring(1).match(regex);
|
|
322
|
+
const params = {};
|
|
323
|
+
|
|
324
|
+
for (let i = 0; i < paramNames.length; i++) {
|
|
325
|
+
params[paramNames[i]] = matches[i + 1];
|
|
326
|
+
}
|
|
327
|
+
if (
|
|
328
|
+
path.includes(":") &&
|
|
329
|
+
window.location.hash.substring(1).split("?")[1]
|
|
330
|
+
) {
|
|
331
|
+
console.error(
|
|
332
|
+
"Cannot use query params with path params",
|
|
333
|
+
path,
|
|
334
|
+
window.location.hash.substring(1).split("?")[1]
|
|
335
|
+
);
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
const query = {};
|
|
339
|
+
|
|
340
|
+
const queryString = window.location.hash.substring(1).split("?")[1];
|
|
341
|
+
if (queryString) {
|
|
342
|
+
const queryParts = queryString.split("&");
|
|
343
|
+
for (let i = 0; i < queryParts.length; i++) {
|
|
344
|
+
const queryParam = queryParts[i].split("=");
|
|
345
|
+
query[queryParam[0]] = queryParam[1];
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const req = {
|
|
349
|
+
params: params,
|
|
350
|
+
query: query,
|
|
351
|
+
url: window.location.hash.substring(1),
|
|
352
|
+
method: "POST",
|
|
353
|
+
};
|
|
354
|
+
window.$URL_QUERY = query;
|
|
355
|
+
window.$URL_PARAMS = params;
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* @alias callback
|
|
359
|
+
* @type {function}
|
|
360
|
+
* @param {Object} req - The request object.
|
|
361
|
+
* @returns {void}
|
|
362
|
+
* @memberof VaderRouter
|
|
363
|
+
* @description Allows you to perform actions when the currentRoute changes.
|
|
364
|
+
*/
|
|
365
|
+
callback(req);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
export default VaderRouter;
|