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 ADDED
@@ -0,0 +1,54 @@
1
+ # VaderJS: A Reactive Framework for Single-Page Applications (SPA)
2
+
3
+ [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Postr-Inc/Vader.js/blob/main/LICENSE) [![npm version](https://img.shields.io/npm/v/vaderjs.svg?style=flat)](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
@@ -0,0 +1,9 @@
1
+ {
2
+ "compilerOptions": {
3
+ "allowJs": true,
4
+ "checkJs": true,
5
+
6
+ },
7
+ "include": ["/"],
8
+
9
+ }
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;