ziko 0.50.2 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ziko.js CHANGED
@@ -2,7 +2,7 @@
2
2
  /*
3
3
  Project: ziko.js
4
4
  Author: Zakaria Elalaoui
5
- Date : Wed Dec 03 2025 10:48:15 GMT+0100 (UTC+01:00)
5
+ Date : Wed Dec 03 2025 10:58:16 GMT+0100 (UTC+01:00)
6
6
  Git-Repo : https://github.com/zakarialaoui10/ziko.js
7
7
  Git-Wiki : https://github.com/zakarialaoui10/ziko.js/wiki
8
8
  Released under MIT License
@@ -2026,38 +2026,137 @@
2026
2026
  }
2027
2027
  }
2028
2028
 
2029
- function ptr_details_setter(){
2030
- switch(this.currentEvent){
2031
- case "pointerdown" : {
2032
- this.dx = parseInt(this.event.offsetX);
2033
- this.dy = parseInt(this.event.offsetY);
2029
+ function ptr_details_setter() {
2030
+ const rect = this.targetElement.getBoundingClientRect();
2031
+ const e = this.event;
2032
+ let x = (e.clientX - rect.left) | 0;
2033
+ let y = (e.clientY - rect.top) | 0;
2034
+
2035
+ if(this.cache.useNormalisedCoordinates){
2036
+ const w = this.targetElement.clientWidth;
2037
+ const h = this.targetElement.clientHeight;
2038
+ x = +((x / w) * 2 - 1).toFixed(8);
2039
+ y = +((y / h) * -2 + 1).toFixed(8);
2040
+ }
2041
+ switch (this.currentEvent) {
2042
+
2043
+ case "pointerdown":
2044
+ this.dx = x;
2045
+ this.dy = y;
2034
2046
  this.isDown = true;
2035
- } break;
2036
- case "pointermove" : {
2037
- this.mx = parseInt(this.event.offsetX);
2038
- this.my = parseInt(this.event.offsetY);
2047
+ break;
2048
+
2049
+ case "pointermove":
2050
+ this.mx = x;
2051
+ this.my = y;
2039
2052
  this.isMoving = true;
2040
- } break;
2041
- case "pointerup" : {
2042
- this.ux = parseInt(this.event.offsetX);
2043
- this.uy = parseInt(this.event.offsetY);
2053
+ break;
2054
+
2055
+ case "pointerup":
2056
+ this.ux = x;
2057
+ this.uy = y;
2044
2058
  this.isDown = false;
2045
- console.log(this.target.width);
2046
- const delta_x=(this.ux-this.dx)/this.target.width;
2047
- const delta_y=(this.dy-this.uy)/this.target.height;
2048
- const HORIZONTAL_SWIPPE=(delta_x<0)?"left":(delta_x>0)?"right":"none";
2049
- const VERTICAL_SWIPPE=(delta_y<0)?"bottom":(delta_y>0)?"top":"none";
2050
- this.swippe={
2051
- h:HORIZONTAL_SWIPPE,
2052
- v:VERTICAL_SWIPPE,
2053
- delta_x,
2054
- delta_y
2055
- };
2056
- } break;
2059
+ this.isMoving = false;
2060
+ break;
2061
+ }
2062
+ }
2063
+
2064
+ function mouse_details_setter() {
2065
+ const rect = this.targetElement.getBoundingClientRect();
2066
+ const e = this.event;
2067
+ let x = (e.clientX - rect.left) | 0;
2068
+ let y = (e.clientY - rect.top) | 0;
2069
+
2070
+ if(this.cache.useNormalisedCoordinates){
2071
+ const w = this.targetElement.clientWidth;
2072
+ const h = this.targetElement.clientHeight;
2073
+ x = +((x / w) * 2 - 1).toFixed(8);
2074
+ y = +((y / h) * -2 + 1).toFixed(8);
2075
+ }
2076
+
2077
+ switch (this.currentEvent) {
2078
+
2079
+ case "mousedown":
2080
+ this.dx = x;
2081
+ this.dy = y;
2082
+ this.isDown = true;
2083
+ break;
2084
+
2085
+ case "mousemove":
2086
+ this.mx = x;
2087
+ this.my = y;
2088
+ this.isMoving = true;
2089
+ break;
2090
+
2091
+ case "mouserup":
2092
+ this.ux = x;
2093
+ this.uy = y;
2094
+ this.isDown = false;
2095
+ this.isMoving = false;
2096
+ break;
2097
+ }
2098
+ }
2099
+
2100
+ function touch_details_setter() {
2101
+ const e = this.event;
2102
+ const touch = e.touches?.[0] || e.changedTouches?.[0];
2103
+
2104
+ if (!touch) return; // should never happen but safe
2105
+
2106
+ const rect = this.targetElement.getBoundingClientRect();
2107
+ let x = touch.clientX - rect.left;
2108
+ let y = touch.clientY - rect.top;
2109
+
2110
+ if(this.cache.useNormalisedCoordinates){
2111
+ const w = this.targetElement.clientWidth;
2112
+ const h = this.targetElement.clientHeight;
2113
+ x = +((x / w) * 2 - 1).toFixed(8);
2114
+ y = +((y / h) * -2 + 1).toFixed(8);
2115
+ }
2116
+
2117
+ switch (this.currentEvent) {
2118
+ case "touchstart":
2119
+ this.dx = x;
2120
+ this.dy = y;
2121
+ this.isDown = true;
2122
+ break;
2123
+
2124
+ case "touchmove":
2125
+ this.mx = x;
2126
+ this.my = y;
2127
+ this.isMoving = true;
2128
+ break;
2129
+
2130
+ case "touchend":
2131
+ this.ux = x;
2132
+ this.uy = y;
2133
+ this.isDown = false;
2134
+ break;
2135
+ }
2136
+ }
2137
+
2138
+ class CoordinatesBasedEvent extends ZikoEvent{
2139
+ constructor(signature, target = null, Events = [], details_setter, customizer){
2140
+ super(signature, target, Events, details_setter, customizer);
2141
+ Object.assign(this.cache,{
2142
+ useNormalisedCoordinates : false
2143
+ });
2144
+ this.isDown = false;
2145
+ this.isMoving = false;
2146
+ this.dx = 0;
2147
+ this.dy = 0;
2148
+ this.mx = 0;
2149
+ this.my = 0;
2150
+ this.ux = 0;
2151
+ this.uy = 0;
2152
+ }
2153
+ get isDragging(){
2154
+ return this.isDown && this.isMoving
2155
+ }
2156
+ useNormalisedCoordinates(enable = true){
2157
+ this.cache.useNormalisedCoordinates = enable;
2158
+ return this;
2057
2159
  }
2058
- // if(this.currentEvent==="click") this.dx = 0
2059
- // else this.dx = 1
2060
- // console.log(this.currentEvent)
2061
2160
  }
2062
2161
 
2063
2162
  class ClickAwayEvent extends Event {
@@ -2288,25 +2387,25 @@
2288
2387
  key_details_setter,
2289
2388
  customizer
2290
2389
  );
2291
- const bind_mouse_event = (target, customizer) => new ZikoEvent(
2390
+ const bind_mouse_event = (target, customizer) => new CoordinatesBasedEvent(
2292
2391
  'mouse',
2293
2392
  target,
2294
2393
  EventsMap.Mouse,
2295
- null,
2394
+ mouse_details_setter,
2296
2395
  customizer
2297
2396
  );
2298
- const bind_pointer_event = (target, customizer) => new ZikoEvent(
2397
+ const bind_pointer_event = (target, customizer) => new CoordinatesBasedEvent(
2299
2398
  'ptr',
2300
2399
  target,
2301
2400
  EventsMap.Ptr,
2302
2401
  ptr_details_setter,
2303
2402
  customizer
2304
2403
  );
2305
- const bind_touch_event = (target, customizer) => new ZikoEvent(
2404
+ const bind_touch_event = (target, customizer) => new CoordinatesBasedEvent(
2306
2405
  'touch',
2307
2406
  target,
2308
2407
  EventsMap.Touch,
2309
- null,
2408
+ touch_details_setter,
2310
2409
  customizer
2311
2410
  );
2312
2411
  const bind_wheel_event = (target, customizer) => new ZikoEvent(
package/dist/ziko.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  /*
3
3
  Project: ziko.js
4
4
  Author: Zakaria Elalaoui
5
- Date : Wed Dec 03 2025 10:48:15 GMT+0100 (UTC+01:00)
5
+ Date : Wed Dec 03 2025 10:58:16 GMT+0100 (UTC+01:00)
6
6
  Git-Repo : https://github.com/zakarialaoui10/ziko.js
7
7
  Git-Wiki : https://github.com/zakarialaoui10/ziko.js/wiki
8
8
  Released under MIT License
@@ -2020,38 +2020,137 @@ function key_details_setter(){
2020
2020
  }
2021
2021
  }
2022
2022
 
2023
- function ptr_details_setter(){
2024
- switch(this.currentEvent){
2025
- case "pointerdown" : {
2026
- this.dx = parseInt(this.event.offsetX);
2027
- this.dy = parseInt(this.event.offsetY);
2023
+ function ptr_details_setter() {
2024
+ const rect = this.targetElement.getBoundingClientRect();
2025
+ const e = this.event;
2026
+ let x = (e.clientX - rect.left) | 0;
2027
+ let y = (e.clientY - rect.top) | 0;
2028
+
2029
+ if(this.cache.useNormalisedCoordinates){
2030
+ const w = this.targetElement.clientWidth;
2031
+ const h = this.targetElement.clientHeight;
2032
+ x = +((x / w) * 2 - 1).toFixed(8);
2033
+ y = +((y / h) * -2 + 1).toFixed(8);
2034
+ }
2035
+ switch (this.currentEvent) {
2036
+
2037
+ case "pointerdown":
2038
+ this.dx = x;
2039
+ this.dy = y;
2028
2040
  this.isDown = true;
2029
- } break;
2030
- case "pointermove" : {
2031
- this.mx = parseInt(this.event.offsetX);
2032
- this.my = parseInt(this.event.offsetY);
2041
+ break;
2042
+
2043
+ case "pointermove":
2044
+ this.mx = x;
2045
+ this.my = y;
2033
2046
  this.isMoving = true;
2034
- } break;
2035
- case "pointerup" : {
2036
- this.ux = parseInt(this.event.offsetX);
2037
- this.uy = parseInt(this.event.offsetY);
2047
+ break;
2048
+
2049
+ case "pointerup":
2050
+ this.ux = x;
2051
+ this.uy = y;
2038
2052
  this.isDown = false;
2039
- console.log(this.target.width);
2040
- const delta_x=(this.ux-this.dx)/this.target.width;
2041
- const delta_y=(this.dy-this.uy)/this.target.height;
2042
- const HORIZONTAL_SWIPPE=(delta_x<0)?"left":(delta_x>0)?"right":"none";
2043
- const VERTICAL_SWIPPE=(delta_y<0)?"bottom":(delta_y>0)?"top":"none";
2044
- this.swippe={
2045
- h:HORIZONTAL_SWIPPE,
2046
- v:VERTICAL_SWIPPE,
2047
- delta_x,
2048
- delta_y
2049
- };
2050
- } break;
2053
+ this.isMoving = false;
2054
+ break;
2055
+ }
2056
+ }
2057
+
2058
+ function mouse_details_setter() {
2059
+ const rect = this.targetElement.getBoundingClientRect();
2060
+ const e = this.event;
2061
+ let x = (e.clientX - rect.left) | 0;
2062
+ let y = (e.clientY - rect.top) | 0;
2063
+
2064
+ if(this.cache.useNormalisedCoordinates){
2065
+ const w = this.targetElement.clientWidth;
2066
+ const h = this.targetElement.clientHeight;
2067
+ x = +((x / w) * 2 - 1).toFixed(8);
2068
+ y = +((y / h) * -2 + 1).toFixed(8);
2069
+ }
2070
+
2071
+ switch (this.currentEvent) {
2072
+
2073
+ case "mousedown":
2074
+ this.dx = x;
2075
+ this.dy = y;
2076
+ this.isDown = true;
2077
+ break;
2078
+
2079
+ case "mousemove":
2080
+ this.mx = x;
2081
+ this.my = y;
2082
+ this.isMoving = true;
2083
+ break;
2084
+
2085
+ case "mouserup":
2086
+ this.ux = x;
2087
+ this.uy = y;
2088
+ this.isDown = false;
2089
+ this.isMoving = false;
2090
+ break;
2091
+ }
2092
+ }
2093
+
2094
+ function touch_details_setter() {
2095
+ const e = this.event;
2096
+ const touch = e.touches?.[0] || e.changedTouches?.[0];
2097
+
2098
+ if (!touch) return; // should never happen but safe
2099
+
2100
+ const rect = this.targetElement.getBoundingClientRect();
2101
+ let x = touch.clientX - rect.left;
2102
+ let y = touch.clientY - rect.top;
2103
+
2104
+ if(this.cache.useNormalisedCoordinates){
2105
+ const w = this.targetElement.clientWidth;
2106
+ const h = this.targetElement.clientHeight;
2107
+ x = +((x / w) * 2 - 1).toFixed(8);
2108
+ y = +((y / h) * -2 + 1).toFixed(8);
2109
+ }
2110
+
2111
+ switch (this.currentEvent) {
2112
+ case "touchstart":
2113
+ this.dx = x;
2114
+ this.dy = y;
2115
+ this.isDown = true;
2116
+ break;
2117
+
2118
+ case "touchmove":
2119
+ this.mx = x;
2120
+ this.my = y;
2121
+ this.isMoving = true;
2122
+ break;
2123
+
2124
+ case "touchend":
2125
+ this.ux = x;
2126
+ this.uy = y;
2127
+ this.isDown = false;
2128
+ break;
2129
+ }
2130
+ }
2131
+
2132
+ class CoordinatesBasedEvent extends ZikoEvent{
2133
+ constructor(signature, target = null, Events = [], details_setter, customizer){
2134
+ super(signature, target, Events, details_setter, customizer);
2135
+ Object.assign(this.cache,{
2136
+ useNormalisedCoordinates : false
2137
+ });
2138
+ this.isDown = false;
2139
+ this.isMoving = false;
2140
+ this.dx = 0;
2141
+ this.dy = 0;
2142
+ this.mx = 0;
2143
+ this.my = 0;
2144
+ this.ux = 0;
2145
+ this.uy = 0;
2146
+ }
2147
+ get isDragging(){
2148
+ return this.isDown && this.isMoving
2149
+ }
2150
+ useNormalisedCoordinates(enable = true){
2151
+ this.cache.useNormalisedCoordinates = enable;
2152
+ return this;
2051
2153
  }
2052
- // if(this.currentEvent==="click") this.dx = 0
2053
- // else this.dx = 1
2054
- // console.log(this.currentEvent)
2055
2154
  }
2056
2155
 
2057
2156
  class ClickAwayEvent extends Event {
@@ -2282,25 +2381,25 @@ const bind_key_event = (target, customizer) => new ZikoEvent(
2282
2381
  key_details_setter,
2283
2382
  customizer
2284
2383
  );
2285
- const bind_mouse_event = (target, customizer) => new ZikoEvent(
2384
+ const bind_mouse_event = (target, customizer) => new CoordinatesBasedEvent(
2286
2385
  'mouse',
2287
2386
  target,
2288
2387
  EventsMap.Mouse,
2289
- null,
2388
+ mouse_details_setter,
2290
2389
  customizer
2291
2390
  );
2292
- const bind_pointer_event = (target, customizer) => new ZikoEvent(
2391
+ const bind_pointer_event = (target, customizer) => new CoordinatesBasedEvent(
2293
2392
  'ptr',
2294
2393
  target,
2295
2394
  EventsMap.Ptr,
2296
2395
  ptr_details_setter,
2297
2396
  customizer
2298
2397
  );
2299
- const bind_touch_event = (target, customizer) => new ZikoEvent(
2398
+ const bind_touch_event = (target, customizer) => new CoordinatesBasedEvent(
2300
2399
  'touch',
2301
2400
  target,
2302
2401
  EventsMap.Touch,
2303
- null,
2402
+ touch_details_setter,
2304
2403
  customizer
2305
2404
  );
2306
2405
  const bind_wheel_event = (target, customizer) => new ZikoEvent(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ziko",
3
- "version": "0.50.2",
3
+ "version": "0.51.0",
4
4
  "description": "A versatile JavaScript library offering a rich set of Hyperscript Based UI components, advanced mathematical utilities, interactivity ,animations, client side routing and more ...",
5
5
  "keywords": [
6
6
  "front-end",
@@ -1 +1,2 @@
1
1
  export const is_primitive = value => typeof value !== 'object' && typeof value !== 'function' || value === null;
2
+ export const is_async = fn => fn && fn.constructor && fn.constructor.name === "AsyncFunction";
@@ -0,0 +1,25 @@
1
+ import { ZikoEvent } from "../ziko-event.js";
2
+
3
+ export class CoordinatesBasedEvent extends ZikoEvent{
4
+ constructor(signature, target = null, Events = [], details_setter, customizer){
5
+ super(signature, target, Events, details_setter, customizer)
6
+ Object.assign(this.cache,{
7
+ useNormalisedCoordinates : false
8
+ })
9
+ this.isDown = false;
10
+ this.isMoving = false;
11
+ this.dx = 0;
12
+ this.dy = 0;
13
+ this.mx = 0;
14
+ this.my = 0;
15
+ this.ux = 0;
16
+ this.uy = 0;
17
+ }
18
+ get isDragging(){
19
+ return this.isDown && this.isMoving
20
+ }
21
+ useNormalisedCoordinates(enable = true){
22
+ this.cache.useNormalisedCoordinates = enable;
23
+ return this;
24
+ }
25
+ }
@@ -2,8 +2,13 @@ import { ZikoEvent } from "../ziko-event.js";
2
2
  import { EventsMap } from "../events-map/index.js";
3
3
  import {
4
4
  ptr_details_setter,
5
- key_details_setter
5
+ key_details_setter,
6
+ mouse_details_setter,
7
+ touch_details_setter
6
8
  } from '../details-setter/index.js'
9
+ import {
10
+ CoordinatesBasedEvent
11
+ } from './coordinates-based-event.js'
7
12
  import {
8
13
  register_click_away_event,
9
14
  register_view_event,
@@ -48,25 +53,25 @@ export const bind_key_event = (target, customizer) => new ZikoEvent(
48
53
  key_details_setter,
49
54
  customizer
50
55
  );
51
- export const bind_mouse_event = (target, customizer) => new ZikoEvent(
56
+ export const bind_mouse_event = (target, customizer) => new CoordinatesBasedEvent(
52
57
  'mouse',
53
58
  target,
54
59
  EventsMap.Mouse,
55
- null,
60
+ mouse_details_setter,
56
61
  customizer
57
62
  );
58
- export const bind_pointer_event = (target, customizer) => new ZikoEvent(
63
+ export const bind_pointer_event = (target, customizer) => new CoordinatesBasedEvent(
59
64
  'ptr',
60
65
  target,
61
66
  EventsMap.Ptr,
62
67
  ptr_details_setter,
63
68
  customizer
64
69
  );
65
- export const bind_touch_event = (target, customizer) => new ZikoEvent(
70
+ export const bind_touch_event = (target, customizer) => new CoordinatesBasedEvent(
66
71
  'touch',
67
72
  target,
68
73
  EventsMap.Touch,
69
- null,
74
+ touch_details_setter,
70
75
  customizer
71
76
  );
72
77
  export const bind_wheel_event = (target, customizer) => new ZikoEvent(
@@ -1,2 +1,4 @@
1
1
  export * from './key.js'
2
- export * from './pointer.js'
2
+ export * from './pointer.js'
3
+ export * from './mouse.js'
4
+ export * from './touch.js'
@@ -0,0 +1,35 @@
1
+ export function mouse_details_setter() {
2
+ const rect = this.targetElement.getBoundingClientRect();
3
+ const e = this.event;
4
+ let x = (e.clientX - rect.left) | 0;
5
+ let y = (e.clientY - rect.top) | 0;
6
+
7
+ if(this.cache.useNormalisedCoordinates){
8
+ const w = this.targetElement.clientWidth;
9
+ const h = this.targetElement.clientHeight;
10
+ x = +((x / w) * 2 - 1).toFixed(8);
11
+ y = +((y / h) * -2 + 1).toFixed(8);
12
+ }
13
+
14
+ switch (this.currentEvent) {
15
+
16
+ case "mousedown":
17
+ this.dx = x;
18
+ this.dy = y;
19
+ this.isDown = true;
20
+ break;
21
+
22
+ case "mousemove":
23
+ this.mx = x;
24
+ this.my = y;
25
+ this.isMoving = true;
26
+ break;
27
+
28
+ case "mouserup":
29
+ this.ux = x;
30
+ this.uy = y;
31
+ this.isDown = false;
32
+ this.isMoving = false;
33
+ break;
34
+ }
35
+ }
@@ -1,33 +1,35 @@
1
- export function ptr_details_setter(){
2
- switch(this.currentEvent){
3
- case "pointerdown" : {
4
- this.dx = parseInt(this.event.offsetX);
5
- this.dy = parseInt(this.event.offsetY);
6
- this.isDown = true
7
- }; break;
8
- case "pointermove" : {
9
- this.mx = parseInt(this.event.offsetX);
10
- this.my = parseInt(this.event.offsetY);
11
- this.isMoving = true
12
- }; break;
13
- case "pointerup" : {
14
- this.ux = parseInt(this.event.offsetX);
15
- this.uy = parseInt(this.event.offsetY);
1
+ export function ptr_details_setter() {
2
+ const rect = this.targetElement.getBoundingClientRect();
3
+ const e = this.event;
4
+ let x = (e.clientX - rect.left) | 0;
5
+ let y = (e.clientY - rect.top) | 0;
6
+
7
+ if(this.cache.useNormalisedCoordinates){
8
+ const w = this.targetElement.clientWidth;
9
+ const h = this.targetElement.clientHeight;
10
+ x = +((x / w) * 2 - 1).toFixed(8);
11
+ y = +((y / h) * -2 + 1).toFixed(8);
12
+ }
13
+ switch (this.currentEvent) {
14
+
15
+ case "pointerdown":
16
+ this.dx = x;
17
+ this.dy = y;
18
+ this.isDown = true;
19
+ break;
20
+
21
+ case "pointermove":
22
+ this.mx = x;
23
+ this.my = y;
24
+ this.isMoving = true;
25
+ break;
26
+
27
+ case "pointerup":
28
+ this.ux = x;
29
+ this.uy = y;
16
30
  this.isDown = false;
17
- console.log(this.target.width)
18
- const delta_x=(this.ux-this.dx)/this.target.width;
19
- const delta_y=(this.dy-this.uy)/this.target.height;
20
- const HORIZONTAL_SWIPPE=(delta_x<0)?"left":(delta_x>0)?"right":"none";
21
- const VERTICAL_SWIPPE=(delta_y<0)?"bottom":(delta_y>0)?"top":"none";
22
- this.swippe={
23
- h:HORIZONTAL_SWIPPE,
24
- v:VERTICAL_SWIPPE,
25
- delta_x,
26
- delta_y
27
- }
28
- }; break;
31
+ this.isMoving = false;
32
+ break;
29
33
  }
30
- // if(this.currentEvent==="click") this.dx = 0
31
- // else this.dx = 1
32
- // console.log(this.currentEvent)
33
- }
34
+ }
35
+
@@ -0,0 +1,37 @@
1
+ export function touch_details_setter() {
2
+ const e = this.event;
3
+ const touch = e.touches?.[0] || e.changedTouches?.[0];
4
+
5
+ if (!touch) return; // should never happen but safe
6
+
7
+ const rect = this.targetElement.getBoundingClientRect();
8
+ let x = touch.clientX - rect.left;
9
+ let y = touch.clientY - rect.top;
10
+
11
+ if(this.cache.useNormalisedCoordinates){
12
+ const w = this.targetElement.clientWidth;
13
+ const h = this.targetElement.clientHeight;
14
+ x = +((x / w) * 2 - 1).toFixed(8);
15
+ y = +((y / h) * -2 + 1).toFixed(8);
16
+ }
17
+
18
+ switch (this.currentEvent) {
19
+ case "touchstart":
20
+ this.dx = x;
21
+ this.dy = y;
22
+ this.isDown = true;
23
+ break;
24
+
25
+ case "touchmove":
26
+ this.mx = x;
27
+ this.my = y;
28
+ this.isMoving = true;
29
+ break;
30
+
31
+ case "touchend":
32
+ this.ux = x;
33
+ this.uy = y;
34
+ this.isDown = false;
35
+ break;
36
+ }
37
+ }
@@ -0,0 +1,46 @@
1
+ import {
2
+ get_root,
3
+ normalize_path,
4
+ routes_matcher,
5
+ is_dynamic,
6
+ dynamic_routes_parser
7
+ } from "../utils/index.js"
8
+ export async function createSPAFileBasedRouter(
9
+ pages,
10
+ target = globalThis?.document?.body
11
+ ) {
12
+ if(!(target instanceof HTMLElement) && target?.element instanceof HTMLElement) target = target?.element;
13
+ if (!(target instanceof HTMLElement)) {
14
+ throw new Error("Invalid mount target: must be HTMLElement or UIElement");
15
+ }
16
+ let path = decodeURIComponent(globalThis.location.pathname.replace(/\/$/, ''));
17
+ const routes = Object.keys(pages);
18
+ const root = get_root(routes);
19
+
20
+ const pairs = {};
21
+ for (const route of routes) {
22
+ const module = await pages[route]();
23
+ const modComponent = await module.default;
24
+ pairs[normalize_path(route, root)] = modComponent;
25
+ }
26
+
27
+ let mask = null;
28
+ let component = null;
29
+
30
+ for (const [routePath, comp] of Object.entries(pairs)) {
31
+ if (routes_matcher(routePath, `/${path}`)) {
32
+ mask = routePath;
33
+ component = comp;
34
+ break;
35
+ }
36
+ }
37
+
38
+ if (!mask) return; // no route matched
39
+
40
+ const params = is_dynamic(mask) ? dynamic_routes_parser(mask, path) : undefined;
41
+ const mounted = params ? await component(params) : await component();
42
+
43
+ if(mounted instanceof HTMLElement) target.append(mounted);
44
+ else mounted.mount(target);
45
+
46
+ }
@@ -0,0 +1,2 @@
1
+ export * from './file-based-router/index.js'
2
+ export * from './utils/index.js'
@@ -0,0 +1,76 @@
1
+ export function dynamic_routes_parser(mask, route) {
2
+ const maskSegments = mask.split("/").filter(Boolean);
3
+ const routeSegments = route.split("/").filter(Boolean);
4
+ const params = {};
5
+ let i = 0, j = 0;
6
+ while (i < maskSegments.length && j < routeSegments.length) {
7
+ const maskSegment = maskSegments[i];
8
+ const routeSegment = routeSegments[j];
9
+ if (maskSegment.startsWith("[...") && maskSegment.endsWith("]")) {
10
+ const paramName = maskSegment.slice(4, -1);
11
+ const remainingMaskSegments = maskSegments.length - i - 1;
12
+ if (remainingMaskSegments === 0) {
13
+ params[paramName] = routeSegments.slice(j).join("/");
14
+ break;
15
+ }
16
+ let requiredSegments = 0;
17
+ for (let k = i + 1; k < maskSegments.length; k++) {
18
+ if (!maskSegments[k].endsWith("]+")) requiredSegments++;
19
+ }
20
+ const remainingRouteSegments = routeSegments.length - j;
21
+ const segmentsToConsume = remainingRouteSegments - requiredSegments;
22
+ if (segmentsToConsume >= 1) {
23
+ params[paramName] = routeSegments
24
+ .slice(j, j + segmentsToConsume)
25
+ .join("/");
26
+ j += segmentsToConsume;
27
+ }
28
+ else return {};
29
+ i++;
30
+ continue;
31
+ }
32
+ if (maskSegment.startsWith("[") && maskSegment.endsWith("]+")) {
33
+ const paramName = maskSegment.slice(1, -2);
34
+ if (routeSegment) {
35
+ params[paramName] = routeSegment;
36
+ j++;
37
+ }
38
+ i++;
39
+ continue;
40
+ }
41
+ if (maskSegment.startsWith("[") && maskSegment.endsWith("]")) {
42
+ const paramName = maskSegment.slice(1, -1);
43
+ params[paramName] = routeSegment;
44
+ }
45
+ else if (maskSegment !== routeSegment) return {};
46
+ i++;
47
+ j++;
48
+ }
49
+ return params;
50
+ }
51
+
52
+
53
+ // console.log("\n=== PARSER TESTS ===");
54
+ // console.log(dynamic_routes_parser("/user/[id]+", "/user"));
55
+ // // 👉 {}
56
+
57
+ // console.log(dynamic_routes_parser("/user/[id]+", "/user/42"));
58
+ // // 👉 { id: "42" }
59
+
60
+ // console.log(dynamic_routes_parser("/blog/[...slug]", "/blog/2025/oct/post"));
61
+ // // 👉 { slug: "2025/oct/post" }
62
+
63
+ // console.log(
64
+ // dynamic_routes_parser("/product/[category]/[id]+", "/product/electronics"),
65
+ // );
66
+ // // 👉 { category: "electronics" }
67
+
68
+ // console.log("\n=== FIX TEST ===");
69
+ // console.log(dynamic_routes_parser("/[...slug]/[id]", "/sl1/sl2/9"));
70
+ // // 👉 { slug: "sl1/sl2", id: "9" }
71
+
72
+ // console.log(dynamic_routes_parser("/[slug]/[...id]", "/sl1/id1/id2"));
73
+ // // 👉 { slug: "sl1", id: "id1/id2" }
74
+
75
+ // console.log(dynamic_routes_parser("/blog/lang/[lang]/id/[id]", "/blog/lang/en/id/10"));
76
+ // // 👉 { lang: "en", id: "10" }
@@ -0,0 +1,16 @@
1
+ export function get_root(paths) {
2
+ if (paths.length === 0) return '';
3
+ const splitPaths = paths.map(path => path.split('/'));
4
+ const minLength = Math.min(...splitPaths.map(parts => parts.length));
5
+ let commonParts = [];
6
+ for (let i = 0; i < minLength; i++) {
7
+ const part = splitPaths[0][i];
8
+ if (splitPaths.every(parts => parts[i] === part || parts[i].startsWith('['))) {
9
+ commonParts.push(part);
10
+ }
11
+ else break;
12
+
13
+ }
14
+ const root = commonParts.join('/') + (commonParts.length ? '/' : '');
15
+ return root;
16
+ }
@@ -0,0 +1,5 @@
1
+ export * from './normalize-path.js'
2
+ export * from './routes-matcher.js'
3
+ export * from './routes-grouper.js'
4
+ export * from './dynamic-routes-parser.js'
5
+ export * from './get-root.js'
@@ -0,0 +1,17 @@
1
+ export function normalize_path(inputPath, root = './src/pages', extensions = ['js', 'ts']) {
2
+ if(root.at(-1)==="/") root = root.slice(0, -1)
3
+ const normalizedPath = inputPath.replace(/\\/g, '/')
4
+ // .replace(/\[(\w+)\]/g, '$1/:$1');
5
+ const parts = normalizedPath.split('/');
6
+ const rootParts = root.split('/');
7
+ const rootIndex = parts.indexOf(rootParts[rootParts.length - 1]);
8
+ if (rootIndex !== -1) {
9
+ const subsequentParts = parts.slice(rootIndex + 1);
10
+ const lastPart = subsequentParts[subsequentParts.length - 1];
11
+ const isIndexFile = lastPart === 'index.js' || lastPart === 'index.ts';
12
+ const hasValidExtension = extensions.some(ext => lastPart === `.${ext}` || lastPart.endsWith(`.${ext}`));
13
+ if (isIndexFile) return '/' + (subsequentParts.length > 1 ? subsequentParts.slice(0, -1).join('/') : '');
14
+ if (hasValidExtension) return '/' + subsequentParts.join('/').replace(/\.(js|ts)$/, '');
15
+ }
16
+ return '';
17
+ }
@@ -0,0 +1,22 @@
1
+ export function is_dynamic(path) {
2
+ const DynamicPattern = /(:\w+|\[\.\.\.\w+\]|\[\w+\]\+?)/;
3
+ return DynamicPattern.test(path);
4
+ }
5
+ export function routes_grouper(routeMap) {
6
+ const grouped = {
7
+ static: {},
8
+ dynamic: {},
9
+ };
10
+ for (const [path, value] of Object.entries(routeMap)) {
11
+ if (is_dynamic(path)) {
12
+ const segments = path.split("/").filter(Boolean);
13
+ const optionalIndex = segments.findIndex(seg => seg.endsWith("]+"));
14
+ const hasInvalidOptional =
15
+ optionalIndex !== -1 && optionalIndex !== segments.length - 1;
16
+ if (hasInvalidOptional) throw new Error(`Invalid optional param position in route: "${path}" — optional parameters can only appear at the end.`);
17
+ grouped.dynamic[path] = value;
18
+ }
19
+ else grouped.static[path] = value;
20
+ }
21
+ return grouped;
22
+ }
@@ -0,0 +1,60 @@
1
+ export function routes_matcher(mask, route) {
2
+ const maskSegments = mask.split("/").filter(Boolean);
3
+ const routeSegments = route.split("/").filter(Boolean);
4
+ let i = 0, j = 0;
5
+ while (i < maskSegments.length && j < routeSegments.length) {
6
+ const maskSegment = maskSegments[i];
7
+ const routeSegment = routeSegments[j];
8
+ if (maskSegment.startsWith("[...") && maskSegment.endsWith("]")) {
9
+ const remainingMaskSegments = maskSegments.length - i - 1;
10
+ if (remainingMaskSegments === 0) return true;
11
+ // Calculate minimum required route segments for remaining mask
12
+ let requiredSegments = 0;
13
+ for (let k = i + 1; k < maskSegments.length; k++) {
14
+ if (!maskSegments[k].endsWith("]+")) {
15
+ requiredSegments++;
16
+ }
17
+ }
18
+ const remainingRouteSegments = routeSegments.length - j;
19
+ if (remainingRouteSegments < requiredSegments) return false;
20
+ const segmentsToConsume = remainingRouteSegments - requiredSegments;
21
+ if (segmentsToConsume < 1) return false;
22
+ j += segmentsToConsume;
23
+ i++;
24
+ continue;
25
+ }
26
+ if (maskSegment.startsWith("[") && maskSegment.endsWith("]+")) {
27
+ if (routeSegment) j++;
28
+ i++;
29
+ continue;
30
+ }
31
+ if (maskSegment.startsWith("[") && maskSegment.endsWith("]")) {
32
+ i++;
33
+ j++;
34
+ continue;
35
+ }
36
+ if (maskSegment !== routeSegment) return false;
37
+ i++;
38
+ j++;
39
+ }
40
+ while (i < maskSegments.length) {
41
+ const seg = maskSegments[i];
42
+ if (seg.endsWith("]+")) {
43
+ i++;
44
+ continue;
45
+ }
46
+ return false;
47
+ }
48
+ return i === maskSegments.length && j === routeSegments.length;
49
+ }
50
+
51
+
52
+ // // DEMO
53
+ // console.log("=== EXISTING TESTS ===");
54
+ // console.log(routes_matcher("/user/[id]+", "/user")); // true
55
+ // console.log(routes_matcher("/user/[id]+", "/user/42")); // true
56
+ // console.log(routes_matcher("/blog/[...slug]", "/blog/a/b")); // true
57
+ // console.log(routes_matcher("/blog/[id]", "/blog")); // false
58
+ // console.log(routes_matcher("/product/:id", "/product/99")); // true
59
+
60
+
package/types/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export type * from './math'
2
2
  export type * from './time'
3
+ export type * from './router'
3
4
  export type * from './hooks'
4
5
  export type * from './data'
@@ -0,0 +1,20 @@
1
+ // createSPAFileBasedRouter.d.ts
2
+
3
+ import { UIElement } from '../../../src/ui/constructors/UIElement.js'
4
+
5
+ /**
6
+ * Creates a SPA (Single Page Application) file-based router.
7
+ * Automatically loads and mounts the component corresponding to the current path.
8
+ * Supports dynamic routes and parameter extraction.
9
+ *
10
+ * @param pages - An object mapping route paths to async module functions that return a component.
11
+ * Example: { "/user/[id]": () => import("./pages/user/[id].js") }
12
+ * @param target - Optional DOM element to mount the component. Defaults to `document.body`.
13
+ */
14
+ export function createSPAFileBasedRouter(
15
+ pages: Record<
16
+ string,
17
+ () => Promise<{ default: (param? : Record<string, string>) => UIElement }>
18
+ >,
19
+ target?: HTMLElement | UIElement
20
+ ): Promise<void>;
@@ -0,0 +1,2 @@
1
+ export type * from './file-based-router'
2
+ export type * from './utils'
@@ -0,0 +1,14 @@
1
+ // dynamic_routes_parser.d.ts
2
+
3
+ /**
4
+ * Parses a route according to a dynamic mask and returns extracted parameters.
5
+ *
6
+ * @param mask - The dynamic route mask (e.g., "/user/[id]+", "/blog/[...slug]").
7
+ * @param route - The actual route to parse (e.g., "/user/42", "/blog/2025/oct/post").
8
+ * @returns An object mapping parameter names to their corresponding values.
9
+ * Returns an empty object if the route does not match the mask.
10
+ */
11
+ export declare function dynamic_routes_parser(
12
+ mask: string,
13
+ route: string
14
+ ): Record<string, string>;
@@ -0,0 +1,11 @@
1
+ // get_root.d.ts
2
+
3
+ /**
4
+ * Finds the common root path among an array of paths.
5
+ * Dynamic segments (e.g., `[id]`) are considered as matching any segment.
6
+ *
7
+ * @param paths - An array of route paths (e.g., ["/user/42", "/user/99"]).
8
+ * @returns The common root path as a string (e.g., "/user/").
9
+ * Returns an empty string if no common root exists.
10
+ */
11
+ export declare function get_root(paths: string[]): string;
@@ -0,0 +1,5 @@
1
+ export type * from './dynamic-routes-parser.d.ts'
2
+ export type * from './routes-matcher.d.ts'
3
+ export type * from './get-root.d.ts'
4
+ export type * from './routes-grouper.d.ts'
5
+ export type * from './normalize-path.d.ts'
@@ -0,0 +1,15 @@
1
+ // normalize_path.d.ts
2
+
3
+ /**
4
+ * Normalizes a file path into a route path.
5
+ *
6
+ * @param inputPath - The file path to normalize (e.g., "./src/pages/user/[id].ts").
7
+ * @param root - The root directory to consider as the base (default: "./src/pages").
8
+ * @param extensions - Array of valid file extensions (default: ["js", "ts"]).
9
+ * @returns A normalized route path (e.g., "/user/[id]") or an empty string if it cannot be normalized.
10
+ */
11
+ export declare function normalize_path(
12
+ inputPath: string,
13
+ root?: string,
14
+ extensions?: string[]
15
+ ): string;
@@ -0,0 +1,29 @@
1
+ // routes_utils.d.ts
2
+
3
+ /**
4
+ * Checks if a route path is dynamic.
5
+ * Dynamic segments include:
6
+ * - Parameters like "[id]"
7
+ * - Catch-all segments like "[...slug]"
8
+ * - Optional segments like "[id]+"
9
+ *
10
+ * @param path - The route path to check.
11
+ * @returns `true` if the path is dynamic, otherwise `false`.
12
+ */
13
+ export function is_dynamic(path: string): boolean;
14
+
15
+ /**
16
+ * Groups routes into static and dynamic categories.
17
+ * Throws an error if an optional parameter appears anywhere but the end of the path.
18
+ *
19
+ * @param routeMap - An object mapping route paths to their handlers/values.
20
+ * @returns An object with two properties:
21
+ * - `static`: Routes with no dynamic segments.
22
+ * - `dynamic`: Routes with dynamic segments.
23
+ */
24
+ export function routes_grouper<T>(
25
+ routeMap: Record<string, T>
26
+ ): {
27
+ static: Record<string, T>;
28
+ dynamic: Record<string, T>;
29
+ };
@@ -0,0 +1 @@
1
+ export declare function routes_matcher(mask: string, route: string): boolean;