vaderjs 1.3.2 → 1.3.3-alpha-1

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/vader.js DELETED
@@ -1,1335 +0,0 @@
1
- let dom = [];
2
- let states = {};
3
- let worker = new Worker(new URL("./worker.js", import.meta.url));
4
- /**
5
- * @function markdown
6
- * @param {String} content
7
- * @description Allows you to convert markdown to html
8
- */
9
- function markdown(content) {
10
- let headers = content.match(/(#+)(.*)/g);
11
- if (headers) {
12
- headers.forEach((header) => {
13
- if (
14
- header.includes("/") ||
15
- header.includes("<") ||
16
- header.includes(">")
17
- ) {
18
- return;
19
- }
20
- let level = header.split("#").length;
21
- content = content.replace(
22
- header,
23
- `<h${level} class="markdown_heading">${header.replace(
24
- /#/g,
25
- ""
26
- )}</h${level}>`
27
- );
28
- });
29
- }
30
-
31
- content = content.replace(/\*\*(.*?)\*\*/g, (match, text) => {
32
- return `<b class="markdown_bold">${text}</b>`;
33
- });
34
- content = content.replace(/\*(.*?)\*/g, (match, text) => {
35
- return `<i class="markdown_italic">${text}</i>`;
36
- });
37
- content = content.replace(/`(.*?)`/g, (match, text) => {
38
- return `<code>${text}</code>`;
39
- });
40
- content = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match, text, url) => {
41
- return `<a class="markdown_link" href="${url}">${text}</a>`;
42
- });
43
- content = content.replace(/!\[([^\]]+)\]\(([^)]+)\)/g, (match, alt, src) => {
44
- return `<img class="markdown_image" src="${src}" alt="${alt}" />`;
45
- });
46
- content = content
47
- .split("\n")
48
- .map((line, index, arr) => {
49
- if (line.match(/^\s*-\s+(.*?)$/gm)) {
50
- if (index === 0 || !arr[index - 1].match(/^\s*-\s+(.*?)$/gm)) {
51
- return `<ul class="markdown_unordered" style="list-style-type:disc;list-style:inside"><li>${line.replace(
52
- /^\s*-\s+(.*?)$/gm,
53
- "$1"
54
- )}</li>`;
55
- } else if (
56
- index === arr.length - 1 ||
57
- !arr[index + 1].match(/^\s*-\s+(.*?)$/gm)
58
- ) {
59
- return `<li>${line.replace(/^\s*-\s+(.*?)$/gm, "$1")}</li></ul>`;
60
- } else {
61
- return `<li>${line.replace(/^\s*-\s+(.*?)$/gm, "$1")}</li>`;
62
- }
63
- } else {
64
- return line;
65
- }
66
- })
67
- .join("\n");
68
-
69
- content = content
70
- .split("\n")
71
- .map((line, index, arr) => {
72
- if (line.match(/^\s*\d+\.\s+(.*?)$/gm)) {
73
- if (index === 0 || !arr[index - 1].match(/^\s*\d+\.\s+(.*?)$/gm)) {
74
- return `<ol class="markdown_ordered" style="list-style-type:decimal;"><li>${line.replace(
75
- /^\s*\d+\.\s+(.*?)$/gm,
76
- "$1"
77
- )}</li>`;
78
- } else if (
79
- index === arr.length - 1 ||
80
- !arr[index + 1].match(/^\s*\d+\.\s+(.*?)$/gm)
81
- ) {
82
- return `<li>${line.replace(/^\s*\d+\.\s+(.*?)$/gm, "$1")}</li></ol>`;
83
- } else {
84
- return `<li>${line.replace(/^\s*\d+\.\s+(.*?)$/gm, "$1")}</li>`;
85
- }
86
- } else {
87
- return line;
88
- }
89
- })
90
- .join("\n");
91
-
92
- return content;
93
- }
94
-
95
- /**
96
- * @function useRef
97
- * @description Allows you to get reference to DOM element
98
- * @param {String} ref
99
- * @returns {void | Object} {current, update}
100
- */
101
-
102
- export const useRef = (ref) => {
103
- const element = document.querySelector(`[ref="${ref}"]`);
104
- const getElement = () => element;
105
-
106
- const update = (data) => {
107
- const newDom = new DOMParser().parseFromString(data, "text/html");
108
- const newElement = newDom.body.firstChild;
109
-
110
- if (element) {
111
- // @ts-ignore
112
- const isDifferent = !newElement.isEqualNode(element);
113
- if (isDifferent) {
114
- // @ts-ignore
115
- element.parentNode.replaceChild(newElement, element);
116
- }
117
- }
118
- };
119
-
120
- return {
121
- current: getElement(),
122
- update
123
- };
124
- };
125
-
126
- let components = [];
127
- /**
128
- * @class Component
129
- * @description Allows you to create a component
130
- * @returns {void}
131
- * @example
132
- * import { Vader } from "../../dist/vader/index.js";
133
- * export class Home extends Vader.Component {
134
- * constructor() {
135
- * super();
136
- * }
137
- * async render() {
138
- * return this.html(`
139
- * <div className="hero p-5">
140
- * <h1>Home</h1>
141
- * </div>
142
- * `);
143
- * }
144
- * }
145
- */
146
- export class Component {
147
- constructor() {
148
- this.states = {};
149
- //@ts-ignore
150
- this.name = this.constructor.name;
151
- this.executedEffects = {};
152
- this.storedProps = {};
153
- this.componentMounted = false;
154
- this.hasMounted = false;
155
- this.$_signal_subscribers = [];
156
- /**
157
- * @property {Array} $_signal_subscribers_ran
158
- * @description Allows you to keep track of signal subscribers
159
- * @private
160
- */
161
- this.$_signal_subscribers_ran = [];
162
- this.effects = {};
163
- this.$_useStore_subscribers = [];
164
- this.init();
165
- this.Componentcontent = null;
166
- this.$_signal_dispatch_event = new CustomEvent("SignalDispatch", {
167
- detail: {
168
- hasUpdated: false,
169
- state: null
170
- }
171
- });
172
- /**
173
- * @property {Object} $_signal_dispatch_cleanup_event
174
- * @description Allows you to dispatch a signal cleanup event
175
- * @private
176
- */
177
- this.$_signal_dispatch_cleanup_event = new CustomEvent(
178
- "Signal_Cleanup_Dispatch",
179
- {
180
- detail: {
181
- state: null,
182
- lastState: null
183
- }
184
- }
185
- );
186
- /**
187
- * @property {Array} snapshots
188
- * @private
189
- */
190
- this.snapshots = [];
191
- /**
192
- * @property {Object} dom
193
- * @description Allows you to get reference to DOM element
194
- * @returns {void | HTMLElement}
195
- *
196
- */
197
- this.dom = [];
198
-
199
- /**
200
- * @property {Boolean} cfr
201
- * @description Allows you to compile html code on the fly - client fly rendering
202
- *
203
- */
204
- this.cfr = false;
205
- /**
206
- * @property {Boolean} worker
207
- * @description Allows you to use a web worker to compile html code on the fly - client fly rendering
208
-
209
- */
210
- }
211
-
212
- /**
213
- * @method adapter
214
- * @description Allows you to create an adapter - this is used to create custom logic
215
- *
216
- *
217
- */
218
- adapter(options) {
219
- // allow you to override the compoent logic
220
-
221
-
222
- }
223
- init() {
224
- this.registerComponent();
225
- }
226
-
227
- registerComponent() {
228
- components.push(this);
229
- }
230
-
231
- /**
232
- * @method setState
233
- * @description Allows you to set state
234
- * @param {String} key
235
- * @param {*} value
236
- * @returns {void}
237
- * @example
238
- * this.setState('count', 1)
239
- * */
240
- setState(key, value) {
241
- this.states[key] = value;
242
- this.updateComponent();
243
- }
244
- /**
245
- * @method componentUnmount
246
- * @description Allows you to run code after component has unmounted
247
- * @type {VoidFunction}
248
- * @returns {void}
249
- */
250
- unmount() {
251
- this.componentMounted = false;
252
- this.componentWillUnmount();
253
- // @ts-ignore
254
- document.querySelector(`[data-component="${this.name}"]`).remove();
255
- if (!document.querySelector(`[data-component="${this.name}"]`)) {
256
- components = components.filter(
257
- (component) => component.name !== this.name
258
- );
259
- }
260
- }
261
-
262
- /**
263
- * @method componentUpdate
264
- * @description Allows you to run code after component has updated
265
- * @param {Object} prev_state
266
- * @param {Object} prev_props
267
- * @param {Object} snapshot
268
- * @returns {void}
269
- * @example
270
- * componentUpdate(prev_state, prev_props, snapshot) {
271
- * console.log(prev_state, prev_props, snapshot)
272
- * }
273
- * */
274
- componentUpdate(prev_state, prev_props, snapshot) {}
275
- /**
276
- * @method componentDidMount
277
- * @description Allows you to run code after component has mounted
278
- */
279
- componentDidMount() {}
280
-
281
- /**
282
- * @method componentWillUnmount
283
- * @description Allows you to run code before component unmounts
284
- * @type {VoidFunction}
285
- * @returns {void}
286
- */
287
- componentWillUnmount() {}
288
-
289
- /**
290
- * @method signal
291
- * @description Allows you to create a signal
292
- * @param {String} key
293
- * @param {any} initialState
294
- * @returns {Object} {subscribe, cleanup, dispatch, call, set, get}
295
- * @example
296
- * let signal = this.signal('count', 0);
297
- * signal.subscribe((value) => {
298
- * console.log(value)
299
- * }, false) // false means it will run every time
300
- * signal.subscribe((value) => {
301
- * console.log(value)
302
- * }, true) // true means it will run once
303
- * signal.call() // this will call all subscribers
304
- * signal.set(1) // this will set the value of the signal
305
- * signal.get() // this will get the value of the signal
306
- * signal.cleanup() // this will remove all subscribers
307
- */
308
- signal = (key, initialState) => {
309
- let hasCaller = false;
310
- let [state, setState] = this.useState(key, initialState, () => {
311
- if (this.$_signal_subscribers.length > 0) {
312
- for (var i = 0; i < this.$_signal_subscribers.length; i++) {
313
- if (!hasCaller) {
314
- if (
315
- this.$_signal_subscribers[i].runonce &&
316
- // @ts-ignore
317
- this.$_signal_subscribers_ran.includes(
318
- this.$_signal_subscribers[i]
319
- )
320
- ) {
321
- break;
322
- } else {
323
- this.$_signal_subscribers[i].function(state);
324
- this.$_signal_subscribers_ran.push(this.$_signal_subscribers[i]);
325
- return;
326
- }
327
- }
328
- }
329
- } else {
330
- this.$_signal_dispatch_event.detail.hasUpdated = true;
331
- this.$_signal_dispatch_event.detail.state = state;
332
- window.dispatchEvent(this.$_signal_dispatch_event);
333
- }
334
- });
335
- /**
336
- * @function $_signal_subscribe
337
- * @description Allows you to subscribe to a signal
338
- * @param {*} fn
339
- * @param {*} runonce
340
- * @returns {void}
341
- *
342
- */
343
- this.$_signal_subscribe = (fn, runonce) => {
344
- this.$_signal_subscribers.push({
345
- function: fn,
346
- runonce: runonce
347
- });
348
- return fn;
349
- };
350
- this.$_signal_cleanup = (fn) => {
351
- this.lastState = state;
352
- this.$_signal_subscribers = this.$_signal_subscribers.filter(
353
- (subscriber) => subscriber.function !== fn
354
- );
355
- // @ts-ignore
356
- this.$_signal_dispatch_cleanup_event.detail.state = this.states;
357
- // @ts-ignore
358
- this.$_signal_dispatch_cleanup_event.detail.lastState = this.lastState;
359
- window.dispatchEvent(this.$_signal_dispatch_event);
360
- };
361
- this.$_signal_dispatch = () => {
362
- for (var i = 0; i < this.$_signal_subscribers.length; i++) {
363
- if (
364
- this.$_signal_subscribers[i].runonce &&
365
- // @ts-ignore
366
- this.$_signal_subscribers_ran.includes(this.$_signal_subscribers[i])
367
- ) {
368
- break;
369
- } else {
370
- this.$_signal_subscribers[i].function(state);
371
- this.$_signal_subscribers_ran.push(this.$_signal_subscribers[i]);
372
- }
373
- }
374
- };
375
- this.$_signal_get = () => {
376
- return state;
377
- };
378
- this.$_signal_call = () => {
379
- hasCaller = true;
380
- // @ts-ignore
381
- this.$_signal_dispatch();
382
- };
383
- /**
384
- * @function $_signal_set
385
- * @description Allows you to set the value of a signal
386
- * @param {*} detail
387
- */
388
- this.$_signal_set = (detail) => {
389
-
390
- setState(detail);
391
- };
392
-
393
- return {
394
- /**
395
- * @function subscribe
396
- * @description Allows you to subscribe to a signal
397
- * @param {*} fn
398
- * @param {*} runonce
399
- */
400
- subscribe: this.$_signal_subscribe,
401
- /**
402
- * @function cleanup
403
- * @description Allows you to cleanup a signal
404
- * @param {*} fn
405
- * @returns {null}
406
- */
407
- cleanup: this.$_signal_cleanup,
408
- /**
409
- * @function dispatch
410
- * @description Allows you to dispatch a signal
411
- * @returns {null}
412
- */
413
- dispatch: this.$_signal_dispatch,
414
- /**
415
- * @function call
416
- * @description Allows you to call a signal
417
- * @returns {null}
418
- */
419
- call: this.$_signal_call,
420
- /**
421
- * @function set
422
- * @description Allows you to set the value of a signal
423
- * @param {*} detail
424
- * @returns {null}
425
- */
426
- set: this.$_signal_set,
427
- /**
428
- * @function get
429
- * @readonly
430
- * @description Allows you to get the value of a signal
431
- * @returns {any}
432
- */
433
- get: this.$_signal_get
434
- };
435
- };
436
- /**
437
- * @method useAuth
438
- * @description Allows you to create an auth object
439
- * @param {Object} options
440
- * @param {Array} options.rulesets
441
- * @param {Object} options.user
442
- * @returns {Object} {can, hasRole, canWithRole, assignRule, revokeRule, canAnyOf, canAllOf, canGroup}
443
- * @example
444
- * let auth = this.useAuth({
445
- * rulesets: [
446
- * {
447
- * action: 'create',
448
- * condition: (user) => {
449
- * return user.role === 'admin'
450
- * }
451
- * }
452
- * ],
453
- * user: {
454
- * role: 'admin'
455
- * }
456
- * })
457
- * auth.can('create') // true
458
- */
459
- useAuth(options) {
460
- /**@type {Array}**/
461
- let rules = options.rulesets;
462
- if (!rules) {
463
- throw new Error("No rulesets provided");
464
- }
465
- /**@type {Object}**/
466
- let user = options.user;
467
- let auth = {
468
- can: (action) => {
469
- let can = false;
470
- rules.forEach((rule) => {
471
- if (rule.action === action) {
472
- if (rule.condition(user)) {
473
- can = true;
474
- }
475
- }
476
- });
477
- return can;
478
- },
479
- /**
480
- * @function hasRole
481
- * @description Allows you to check if user has a role
482
- * @param {String} role
483
- * @returns {Boolean}
484
- */
485
- hasRole: (role) => {
486
- return user.role && user.role.includes(role);
487
- },
488
- /**
489
- * @function canWithRole
490
- * @param {String} action
491
- * @param {String} role
492
- * @returns
493
- */
494
- canWithRole: (action, role) => {
495
- return auth.can(action) && auth.hasRole(role);
496
- },
497
- assignRule: (rule) => {
498
- if (
499
- !rules.some((existingRule) => existingRule.action === rule.action)
500
- ) {
501
- rules.push(rule);
502
- }
503
- },
504
- revokeRule: (action) => {
505
- rules = rules.filter((rule) => rule.action !== action);
506
- },
507
- canAnyOf: (actions) => {
508
- return actions.some((action) => auth.can(action));
509
- },
510
- canAllOf: (actions) => {
511
- return actions.every((action) => auth.can(action));
512
- },
513
- canGroup: (actions, logicalOperator = "any") => {
514
- return logicalOperator === "any"
515
- ? auth.canAnyOf(actions)
516
- : auth.canAllOf(actions);
517
- }
518
- };
519
- return auth;
520
- }
521
- /**
522
- * @method useReducer
523
- * @description Allows you to create a reducer
524
- * @param {*} key
525
- * @param {*} reducer
526
- * @param {*} initialState
527
- * @url - useReducer works similarly to - https://react.dev/reference/react/useReducer
528
- * @returns {Array} [state, dispatch]
529
- * @example
530
- * let [count, setCount] = this.useReducer('count', (state, action) => {
531
- * switch (action.type) {
532
- * case 'increment':
533
- * return state + 1
534
- * case 'decrement':
535
- * return state - 1
536
- * default:
537
- * throw new Error()
538
- * }
539
- * }, 0)
540
- * setCount({type: 'increment'})
541
- */
542
- useReducer(key, reducer, initialState) {
543
- if (!this.states[key]) {
544
- this.states[key] = initialState;
545
- }
546
- return [
547
- this.states[key],
548
- /**
549
- * @function dispatch
550
- * @description Allows you to dispatch a reducer
551
- * @param {*} action
552
- * @returns {void}
553
- */
554
- (action) => {
555
- this.states[key] = reducer(this.states[key], action);
556
- this.updateComponent();
557
- }
558
- ];
559
- }
560
-
561
- runEffects() {
562
- Object.keys(this.effects).forEach((component) => {
563
- this.effects[component].forEach((effect) => {
564
- if (!this.executedEffects[effect]) {
565
- effect();
566
- this.executedEffects[effect] = true;
567
- }
568
- });
569
- });
570
- }
571
-
572
- /**
573
- * @method useSyncStore
574
- * @description Allows you to create a store
575
- * @param {String} storeName
576
- * @param {*} initialState
577
- * @returns {Object} {getField, setField, subscribe, clear}
578
- * @example
579
- * let store = this.useSyncStore('store', {count: 0})
580
- * store.setField('count', 1)
581
- * store.getField('count') // 1
582
- *
583
- */
584
- useSyncStore(storeName, initialState) {
585
- let [storedState, setStoredState] = this.useState(
586
- storeName,
587
- initialState ||
588
- // @ts-ignore
589
- localStorage.getItem(`$_vader_${storeName}`, (s) => {
590
- localStorage.setItem(`$_vader_${storeName}`, JSON.stringify(s));
591
- this.$_useStore_subscribers.forEach((subscriber) => {
592
- subscriber(s);
593
- });
594
- }) ||
595
- {}
596
- );
597
-
598
- const getField = (fieldName) => {
599
- return storedState[fieldName];
600
- };
601
- const setField = (fieldName, value) => {
602
- const newState = { ...storedState, [fieldName]: value };
603
- setStoredState(newState);
604
- };
605
- const subscribe = (subscriber) => {
606
- return this.$_useStore_subscribers.push(subscriber);
607
- };
608
-
609
- const clear = (fieldName) => {
610
- let newState = storedState;
611
- delete newState[fieldName];
612
- setStoredState(newState);
613
- };
614
- return {
615
- getField,
616
- setField,
617
- subscribe,
618
- clear
619
- };
620
- }
621
- /**
622
- * @method useState
623
- * @description Allows you to create a state
624
- * @param {String} key
625
- * @param {*} initialValue
626
- * @param {*} callback
627
- * @url - useState works similarly to - https://react.dev/reference/react/useState
628
- * @returns {Array} [state, setState]
629
- * @example
630
- * let [count, setCount] = this.useState('count', 0, () => {
631
- * console.log('count has been updated')
632
- * })
633
- *
634
- * setCount(count + 1)
635
- */
636
- useState(key, initialValue, callback = null) {
637
- if (!this.states[key]) {
638
- this.states[key] = initialValue;
639
- }
640
-
641
- return [
642
- this.states[key],
643
- /**
644
- * @function setState
645
- * @description Allows you to set state
646
- * @param {*} value
647
- * @returns {void}
648
- */
649
- (value) => {
650
- this.states[key] = value;
651
- this.updateComponent();
652
- // @ts-ignore
653
- typeof callback === "function" ? callback() : null;
654
- }
655
- ];
656
- }
657
- /**
658
- * @method useRef
659
- * @memberof Component
660
- * @param {string} ref
661
- * @description Allows you to get reference to DOM elements from the dom array
662
- * @returns {Object} {current, update}
663
- * @example
664
- * let ref = this.useRef('ref')
665
- * ref.current // returns the DOM element
666
-
667
- */
668
-
669
- useRef(ref) {
670
- // get ref from array
671
- const element = this.dom[ref];
672
-
673
- const getElement = () => element;
674
-
675
- const update = (data) => {
676
- const newDom = new DOMParser().parseFromString(data, "text/html");
677
- const newElement = newDom.body.firstChild;
678
-
679
- if (element) {
680
- // @ts-ignore
681
- const isDifferent = !newElement.isEqualNode(element);
682
- if (isDifferent) {
683
- // @ts-ignore
684
- element.parentNode.replaceChild(newElement, element);
685
- }
686
- }
687
- };
688
-
689
- return {
690
- /**@type {HTMLElement} */
691
- // @ts-ignore
692
- current: getElement,
693
- /**@type {Function} */
694
- update
695
- };
696
- }
697
-
698
- /**
699
- *
700
- * @function useEffect
701
- * @param {*} effectFn
702
- * @param {*} dependencies
703
- * @description Allows you to run side effects
704
- * @deprecated - this is no longer suggested please use vader signals instead
705
- * @returns {Object} {cleanup}
706
- */
707
- useEffect(effectFn, dependencies) {
708
- if (!this.effects[this.name]) {
709
- this.effects[this.name] = [];
710
- }
711
- this.effects[this.name].push(effectFn);
712
-
713
- if (dependencies.length > 0) {
714
- dependencies.forEach((d) => {
715
- if (d.set) {
716
- throw new Error(
717
- "signal found, do not use effect and signals at the same time - signals are more efficient"
718
- );
719
- }
720
- });
721
- } else if (!this.hasMounted) {
722
- effectFn();
723
- this.hasMounted = true;
724
- }
725
-
726
- return {
727
- cleanup: () => {
728
- this.effects[this.name] = this.effects[this.name].filter(
729
- (effect) => effect !== effectFn
730
- );
731
- }
732
- };
733
- }
734
- /**
735
- * @method $Function
736
- * @description Allows you to create a function in global scope
737
- * @returns {Function}
738
- * @example
739
- * let func = this.$Function(function add(e, a){
740
- * return e + a
741
- * })
742
- * @param {*} fn
743
- */
744
- $Function(fn) {
745
- // @ts-ignore
746
- if (!typeof fn === "function") {
747
- throw new Error("fn must be a function");
748
- }
749
- let name = fn.name;
750
- if (!name) {
751
- name = "anonymous" + Math.floor(Math.random() * 100000000000000000);
752
- }
753
- window[name] = fn;
754
- // @ts-ignore
755
- return `window.${name}()`;
756
- }
757
-
758
- // Add other methods like render, useEffect, useReducer, useAuth, etc.
759
-
760
- updateComponent() {
761
- const fragment = document.createDocumentFragment();
762
- Object.keys(components).forEach(async (component) => {
763
- const { name } = components[component];
764
-
765
- let componentContainer = document.querySelector(
766
- `[data-component="${name}"]`
767
- );
768
- let time = new Date().getTime();
769
- /**
770
- * @property {Object} snapshot
771
- * @description Allows you to keep track of component snapshots
772
- * @private
773
- * @returns {Object} {name, time, prev_state, prev_props, content}
774
- */
775
- let snapshot = {
776
- name: name,
777
- time: time,
778
- prev_state: this.states,
779
- prev_props: this.storedProps,
780
- // @ts-ignore
781
- content: componentContainer.innerHTML
782
- };
783
-
784
- if (!componentContainer) return;
785
- const newHtml = await new Function(
786
- "useState",
787
- "useEffect",
788
- "useAuth",
789
- "useReducer",
790
- "useSyncStore",
791
- "signal",
792
- "rf",
793
- "props",
794
- "render",
795
- "return `" + (await this.render()) + "`;"
796
- )(
797
- this.useState,
798
- this.useEffect,
799
- this.useAuth,
800
- this.useReducer,
801
- this.useSyncStore,
802
- this.signal,
803
- this.render
804
- );
805
-
806
- if (newHtml !== componentContainer.innerHTML) {
807
- if (this.snapshots.length > 0) {
808
- let lastSnapshot = this.snapshots[this.snapshots.length - 1];
809
- if (lastSnapshot !== snapshot) {
810
- this.snapshots.push(snapshot);
811
- }
812
- } else {
813
- this.snapshots.push(snapshot);
814
- }
815
- this.componentUpdate(
816
- snapshot.prev_state,
817
- snapshot.prev_props,
818
- snapshot.content
819
- );
820
- // batch updates
821
- fragment.appendChild(
822
- document.createRange().createContextualFragment(newHtml)
823
- );
824
- componentContainer.innerHTML = "";
825
- componentContainer.appendChild(fragment);
826
- this.runEffects();
827
- }
828
- });
829
- }
830
- /**
831
- * @method validateClassName
832
- * @param {String} className
833
- * @private
834
- * @returns {Boolean}
835
- */
836
- validateClassName(className) {
837
- // validate classNames ensure they are camelCase but also allow for - and _
838
- return /^[a-zA-Z0-9-_]+$/.test(className);
839
- }
840
-
841
- parseHTML(result) {
842
- const dom = new DOMParser().parseFromString(result, "text/html");
843
- const elements = dom.documentElement.querySelectorAll("*");
844
-
845
- elements.forEach((element) => {
846
- switch (element.nodeName) {
847
- case "IMG":
848
- if (
849
- !element.hasAttribute("alt") &&
850
- !document.documentElement.outerHTML
851
- .trim()
852
- .includes("<!-- #vader-disable_accessibility -->")
853
- ) {
854
- throw new SyntaxError(
855
- `Image: ${element.outerHTML} missing alt attribute`
856
- );
857
- } else if (
858
- element.hasAttribute("alt") &&
859
- // @ts-ignore
860
- element.getAttribute("alt").length < 1 &&
861
- !document.documentElement.outerHTML
862
- .trim()
863
- .includes("<!-- #vader-disable_accessibility -->")
864
- ) {
865
- throw new SyntaxError(
866
- `Image: ${element.outerHTML} alt attribute cannot be empty`
867
- );
868
- } else if (
869
- (element.hasAttribute("src") &&
870
- !element.getAttribute("src")?.includes("http")) ||
871
- (!element.getAttribute("src")?.includes("https") &&
872
- !document.documentElement.outerHTML
873
- .trim()
874
- .includes("<!-- #vader-disable_accessibility -->"))
875
- ) {
876
- let prevurl = element.getAttribute("src");
877
- element.setAttribute("aria-hidden", "true");
878
- element.setAttribute("hidden", "true");
879
- // if window.lcoation.pathname includes a html file remove it and only use the path
880
- let url =
881
- window.location.origin +
882
- window.location.pathname.replace(/\/[^\/]*$/, "") +
883
- "/public/" +
884
- element.getAttribute("src");
885
- let image = new Image();
886
- image.src = url;
887
- image.onerror = () => {
888
- // @ts-ignore
889
- element.setAttribute("src", prevurl);
890
- throw new Error(`Image: ${element.outerHTML} not found`);
891
- };
892
- element.setAttribute("src", url);
893
-
894
- image.onload = () => {
895
- document.querySelectorAll(`img[src="${url}"]`).forEach((img) => {
896
- img.setAttribute("src", url);
897
- img.removeAttribute("aria-hidden");
898
- img.removeAttribute("hidden");
899
- });
900
- };
901
- }
902
- break;
903
-
904
- default:
905
- if (element.hasAttribute("ref")) {
906
- // @ts-ignore
907
- dom[element.getAttribute("ref")] = element;
908
- }
909
- if (element.nodeName === "MARKDOWN") {
910
- element.innerHTML = markdown(
911
- element.innerHTML.replace(/\\n/g, "\n").trim()
912
- );
913
- }
914
-
915
- if (element.hasAttribute("class")) {
916
- const allowClassComments = document.documentElement.outerHTML.includes(
917
- "<!-- #vader-allow_class -->"
918
- );
919
- if (!allowClassComments) {
920
- console.warn(
921
- "you can disable class errors using, <!-- #vader-allow_class -->"
922
- );
923
- throw new Error(
924
- "class attribute is not allowed, please use className instead"
925
- );
926
- }
927
- } else if (element.hasAttribute("className")) {
928
- // @ts-ignore
929
- element.setAttribute("class", element.getAttribute("className"));
930
- element.removeAttribute("className");
931
- }
932
-
933
- if (
934
- element.hasAttribute("href") &&
935
- // @ts-ignore
936
- element.getAttribute("href").startsWith("/") &&
937
- !document.documentElement.outerHTML
938
- .trim()
939
- .includes("<!-- #vader-disable_relative-paths -->")
940
- ) {
941
- element.setAttribute(
942
- "href",
943
- // @ts-ignore
944
- `#/${element.getAttribute("href").replace("/", "")}`
945
- );
946
- }
947
-
948
- if (
949
- element.hasAttribute("src") &&
950
- // @ts-ignore
951
- !element.getAttribute("src").includes("http") &&
952
- // @ts-ignore
953
- !element.getAttribute("src").includes("https") &&
954
- !document.documentElement.outerHTML.includes(
955
- `<!-- #vader-disable_relative-paths -->`
956
- )
957
- ) {
958
- element.setAttribute(
959
- "src",
960
- // @ts-ignore
961
- `./public/${element.getAttribute("src")}`
962
- );
963
- }
964
- break;
965
- }
966
- });
967
-
968
- result = dom.body.innerHTML;
969
-
970
- this.Componentcontent = result;
971
-
972
- if (!result.includes("<div data-component")) {
973
- result = `<div data-component="${this.name}">${result}</div>`;
974
- }
975
- return markdown(result.replace(/\\n/g, "\n").trim());
976
- }
977
-
978
- /**
979
- * The `html` method generates and processes HTML content for a component, performing various validations and tasks.
980
- *
981
- * @param {String} strings - The HTML content to be processed.
982
- * @param {...any} args - Dynamic values to be inserted into the template.
983
- * @returns {string} - The processed HTML content as a string.
984
- *
985
- * @throws {SyntaxError} - Throws a `SyntaxError` if image-related attributes are missing or invalid.
986
- * @throws {Error} - Throws an `Error` if there are issues with class names or relative paths.
987
- *
988
- * @example
989
- * // Example usage within a component:
990
- * const myComponent = new Component();
991
- * const htmlContent = myComponent.html`
992
- * <div>
993
- * <img src="/images/example.jpg" alt="Example Image" />
994
- * </div>
995
- * `;
996
- * document.body.innerHTML = htmlContent;
997
- *
998
- * @remarks
999
- * The `html` method is a core function used in component rendering. It allows you to define and generate HTML content within your component while enforcing best practices and accessibility standards. The method performs several essential tasks:
1000
- *
1001
- * 1. **Image Validation**: It checks images for the presence of 'alt' attributes and their validity.
1002
- * - Throws a `SyntaxError` if an image is missing the 'alt' attribute.
1003
- * - Throws a `SyntaxError` if the 'alt' attribute is empty.
1004
- * - Checks for an 'aria-hidden' attribute for image elements.
1005
- *
1006
- * 2. **Class Attribute Handling**: It enforces class attribute usage and allows optional configuration via comments.
1007
- * - Throws an `Error` if 'class' attributes are used without permission.
1008
- * - Supports 'className' attributes for class definitions.
1009
- * - Allows or disallows class-related comments based on your configuration.
1010
- *
1011
- * 3. **Relative Path Handling**: It processes relative paths in 'href' and 'src' attributes, ensuring proper routing.
1012
- * - Converts relative 'href' attributes to anchor links with appropriate routing.
1013
- * - Converts relative 'src' attributes to absolute paths with 'public' directories.
1014
- *
1015
- * 4. **Custom Component Attributes**: It supports adding a 'data-component' attribute to the root element.
1016
- * - Ensures that the 'data-component' attribute is present for component identification.
1017
- *
1018
- * 5. **Lifecycle Method Invocation**: It invokes the `componentDidMount` method if called from a 'render' context.
1019
- * - Executes `componentDidMount` to handle component initialization once the DOM is ready.
1020
- *
1021
- * @see {@link Component}
1022
- * @see {@link Component#componentDidMount}
1023
- */
1024
-
1025
- html(strings, ...args) {
1026
- // @ts-ignore
1027
- let tiemr = setInterval(() => {
1028
- if (document.querySelector(`[data-component="${this.name}"]`)) {
1029
- clearInterval(tiemr);
1030
- this.componentMounted = true;
1031
-
1032
- document
1033
- .querySelector(`[data-component="${this.name}"]`)
1034
- ?.querySelectorAll("*")
1035
- .forEach((element) => {
1036
- if (element.hasAttribute("ref")) {
1037
- // @ts-ignore
1038
- this.dom[element.getAttribute("ref")] = element;
1039
- }
1040
- });
1041
- this.componentDidMount();
1042
- }
1043
- }, 100);
1044
- let script = document.createElement("script");
1045
- script.setAttribute("type", "text/javascript");
1046
- script.setAttribute(`data-component-script`, this.name);
1047
-
1048
- let dom = this.dom;
1049
-
1050
- if (this.cfr) {
1051
- worker.postMessage({
1052
- strings,
1053
- args,
1054
- location:
1055
- window.location.origin +
1056
- window.location.pathname.replace(/\/[^\/]*$/, "") +
1057
- "/public/",
1058
- name: this.name
1059
- });
1060
- let promise = new Promise((resolve, reject) => {
1061
- worker.onmessage = (e) => {
1062
- if (e.data.error) {
1063
- throw new Error(e.data.error);
1064
- }
1065
- const dom = this.dom; // Assuming this.dom is an object
1066
- let js = e.data.js;
1067
- let template = e.data.template;
1068
- // Bind the component's context
1069
-
1070
- const useState = this.useState.bind(this); // Bind the component's context
1071
- const useEffect = this.useEffect.bind(this); // Bind the component's context
1072
- const useReducer = this.useReducer.bind(this); // Bind the component's context
1073
- const useAuth = this.useAuth.bind(this); // Bind the component's context
1074
- const useSyncStore = this.useSyncStore.bind(this); // Bind the component's context
1075
- const signal = this.signal.bind(this); // Bind the component's context
1076
- const rf = this.$Function.bind(this); // Bind the component's context
1077
- let states = this.states;
1078
- const useRef = this.useRef.bind(this); // Bind the component's context
1079
- new Function(
1080
- "useState",
1081
- "useEffect",
1082
- "useAuth",
1083
- "useReducer",
1084
- "useSyncStore",
1085
- "signal",
1086
- "rf",
1087
- "dom",
1088
- "render",
1089
- "states",
1090
- "useRef",
1091
- js
1092
- )(
1093
- useState,
1094
- useEffect,
1095
- useAuth,
1096
- useReducer,
1097
- useSyncStore,
1098
- signal,
1099
- rf,
1100
- this.dom,
1101
- this.render,
1102
- this.states,
1103
- useRef
1104
- );
1105
-
1106
- resolve(
1107
- new Function("useRef", "states", "return" + "`" + template + "`")(
1108
- useRef,
1109
- states
1110
- )
1111
- );
1112
- };
1113
- worker.onerror = (e) => {
1114
- reject(e);
1115
- };
1116
- });
1117
- // @ts-ignore
1118
- return promise;
1119
- } else {
1120
- let result = "";
1121
- for (let i = 0; i < strings.length; i++) {
1122
- result += strings[i];
1123
- if (i < args.length) {
1124
- result += args[i];
1125
- }
1126
- }
1127
- result = new Function("useRef", `return \`${result}\``)(useRef);
1128
-
1129
- if (!result.trim().startsWith("<body>")) {
1130
- console.warn(
1131
- "You should wrap your html in a body tag, vader may not grab all html!"
1132
- );
1133
- }
1134
-
1135
- return this.parseHTML(result);
1136
- }
1137
- }
1138
- // write types to ensure it returns a string
1139
- /**
1140
- * @method render
1141
- * @description Allows you to render html
1142
- * @returns {Promise <any>}
1143
- * @example
1144
- * async render() {
1145
- * return this.html(`
1146
- * <div className="hero p-5">
1147
- * <h1>Home</h1>
1148
- * </div>
1149
- * `);
1150
- */
1151
- async render(props) {}
1152
- }
1153
-
1154
- /**
1155
- * @object Vader
1156
- * @property {class} Component
1157
- * @property {function} useRef
1158
- * @description Allows you to create a component
1159
- * @example
1160
- * import { Vader } from "../../dist/vader/vader.js";
1161
- * export class Home extends Vader.Component {
1162
- * constructor() {
1163
- * super('Home');
1164
- * }
1165
- * async render() {
1166
- * return this.html(`
1167
- * <div className="hero p-5">
1168
- * <h1>Home</h1>
1169
- * </div>
1170
- * `);
1171
- * }
1172
- */
1173
- const Vader = {
1174
- /**
1175
- * @class Component
1176
- * @description Allows you to create a component
1177
- * @returns {void}
1178
- * @memberof {Vader}
1179
- * @example
1180
- * import { Vader } from "../../dist/vader/index.js";
1181
- * export class Home extends Vader.Component {
1182
- * constructor() {
1183
- * super();
1184
- * }
1185
- * async render() {
1186
- * return this.html(`
1187
- * <div className="hero p-5">
1188
- * <h1>Home</h1>
1189
- * </div>
1190
- * `);
1191
- * }
1192
- * }
1193
- */
1194
- Component: Component,
1195
- useRef: useRef
1196
- };
1197
- /**
1198
- * @function component
1199
- * @description Allows you to create a component
1200
- * @returns {Component}
1201
- */
1202
- export const component = () => {
1203
- return new Component()
1204
- };
1205
-
1206
- /**
1207
- * @function rf
1208
- * @param {*} name
1209
- * @param {*} fn
1210
- * @returns {void}
1211
- * @deprecated - rf has been replaced with Vader.Component.$Function
1212
- * @description Allows you to register function in global scope
1213
- */
1214
- export const rf = (name, fn) => {
1215
- window[name] = fn;
1216
- };
1217
- let cache = {};
1218
- async function handletemplate(data) {
1219
- let dom = new DOMParser().parseFromString(data, "text/html");
1220
- let elements = dom.documentElement.querySelectorAll("*");
1221
-
1222
- if (elements.length > 0) {
1223
- for (var i = 0; i < elements.length; i++) {
1224
- if (elements[i].nodeName === "INCLUDE") {
1225
- if (
1226
- !elements[i].getAttribute("src") ||
1227
- elements[i].getAttribute("src") === ""
1228
- ) {
1229
- throw new Error("Include tag must have src attribute");
1230
- }
1231
-
1232
- let componentName = elements[i]
1233
- .getAttribute("src")
1234
- ?.split("/")
1235
- .pop()
1236
- ?.split(".")[0];
1237
- // @ts-ignore
1238
- let filedata = await include(elements[i].getAttribute("src"));
1239
- // replace ` with \`\` to allow for template literals
1240
- filedata = filedata.replace(/`/g, "\\`");
1241
- cache[elements[i].getAttribute("src")] = filedata;
1242
- filedata = new Function(`return \`${filedata}\`;`)();
1243
- let newdom = new DOMParser().parseFromString(filedata, "text/html");
1244
-
1245
-
1246
- newdom.querySelectorAll("include").forEach((el) => {
1247
- el.remove();
1248
- });
1249
- // @ts-ignore
1250
-
1251
- let els = dom.querySelectorAll(componentName);
1252
-
1253
- els.forEach((el) => {
1254
- if (el.attributes.length > 0) {
1255
- for (var i = 0; i < el.attributes.length; i++) {
1256
- // @ts-ignore
1257
- newdom.body.outerHTML = newdom.body.outerHTML.replaceAll(
1258
- `{{${el.attributes[i].name}}}`,
1259
- el.attributes[i].value
1260
- );
1261
- }
1262
- }
1263
- if (el.children.length > 0 && newdom.body.querySelector("slot")) {
1264
- for (var i = 0; i < el.children.length; i++) {
1265
- let slots = newdom.body.querySelectorAll("slot");
1266
- slots.forEach((slot) => {
1267
-
1268
- let id = slot.getAttribute("id");
1269
- console.log(id)
1270
- if(el.hasAttribute('key') && el.getAttribute('key') === id || !el.hasAttribute('key') && el.nodeName === id){
1271
-
1272
- slot.outerHTML = `<div>${el.innerHTML}</div>`
1273
- }
1274
- });
1275
- }
1276
- }
1277
-
1278
- dom.body.querySelectorAll("include").forEach((el) => {
1279
- el.remove();
1280
- });
1281
- // replace ` with \`\` to allow for template literals
1282
- dom.body.outerHTML = dom.body.outerHTML.replace(/`/g, "\\`");
1283
- dom.body.outerHTML = dom.body.outerHTML.replace(
1284
- el.outerHTML,
1285
- new Function(`return \`${newdom.body.outerHTML}\`;`)()
1286
- );
1287
- });
1288
- }
1289
- }
1290
- }
1291
-
1292
- // replace ` with \`\` to allow for template literals
1293
- dom.body.outerHTML = dom.body.outerHTML.replace(/`/g, "\\`");
1294
- data = new Function(`return \`${dom.body.outerHTML}\`;`)();
1295
-
1296
- return data;
1297
- }
1298
- /**
1299
- * @function include
1300
- * @description Allows you to include html file
1301
- * @returns {Promise} - modified string with html content
1302
- * @param {string} path
1303
- */
1304
-
1305
- export const include = async (path) => {
1306
- if (
1307
- path.startsWith("/") &&
1308
- !path.includes("/src/") &&
1309
- !document.documentElement.outerHTML
1310
- .trim()
1311
- .includes("<!-- #vader-disable_relative-paths -->")
1312
- ) {
1313
- path = "/src/" + path;
1314
- }
1315
- if (cache[path]) {
1316
- return await handletemplate(new Function(`return \`${cache[path]}\`;`)());
1317
- } else {
1318
- return fetch(`./${path}`)
1319
- .then((res) => {
1320
- if (res.status === 404) {
1321
- throw new Error(`No file found at ${path}`);
1322
- }
1323
- return res.text();
1324
- })
1325
- .then(async (data) => {
1326
- cache[path] = data;
1327
-
1328
- data = await handletemplate(new Function(`return \`${data}\`;`)());
1329
-
1330
- return data;
1331
- });
1332
- }
1333
- };
1334
-
1335
- export default Vader;