@codeleap/web-navigation 5.4.4
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/package.json +21 -0
- package/package.json.bak +21 -0
- package/src/index.ts +1 -0
- package/src/navigation.tsx +282 -0
- package/src/types.ts +53 -0
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codeleap/web-navigation",
|
|
3
|
+
"version": "5.4.4",
|
|
4
|
+
"main": "src/index.ts",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"repository": {
|
|
7
|
+
"url": "https://github.com/codeleap-uk/internal-libs-monorepo.git",
|
|
8
|
+
"type": "git",
|
|
9
|
+
"directory": "packages/web-navigation"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"ts-node-dev": "1.1.8"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "echo 'No build needed'"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"typescript": "5.5.2",
|
|
19
|
+
"@fastify/deepmerge": "1.3.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/package.json.bak
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codeleap/web-navigation",
|
|
3
|
+
"version": "5.4.4",
|
|
4
|
+
"main": "src/index.ts",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"repository": {
|
|
7
|
+
"url": "https://github.com/codeleap-uk/internal-libs-monorepo.git",
|
|
8
|
+
"type": "git",
|
|
9
|
+
"directory": "packages/web-navigation"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"ts-node-dev": "1.1.8"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "echo 'No build needed'"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"typescript": "5.5.2",
|
|
19
|
+
"@fastify/deepmerge": "1.3.0"
|
|
20
|
+
}
|
|
21
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './navigation'
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { AnyValue, Config, History, Navigator, RouteParams, RoutePath, Routes } from './types'
|
|
2
|
+
import deepmerge from '@fastify/deepmerge'
|
|
3
|
+
|
|
4
|
+
const IS_SSR = typeof window === 'undefined' || typeof history === 'undefined'
|
|
5
|
+
|
|
6
|
+
const defaultConfig: Partial<Config> = {
|
|
7
|
+
historyEnabled: false,
|
|
8
|
+
getMetadata: () => { },
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type {
|
|
12
|
+
Navigator,
|
|
13
|
+
Routes,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class Navigation<O extends object, R extends object = {}, C extends Record<string, any> = {}> {
|
|
17
|
+
private _history: History = {}
|
|
18
|
+
|
|
19
|
+
private config: Config = defaultConfig
|
|
20
|
+
|
|
21
|
+
get history() {
|
|
22
|
+
return this._history
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public context: C = {} as C
|
|
26
|
+
|
|
27
|
+
private putHistory(path: RoutePath, info: any = {}) {
|
|
28
|
+
const idx = Object.keys(this._history).length + 1
|
|
29
|
+
|
|
30
|
+
const origin = IS_SSR ? null : window?.location?.origin
|
|
31
|
+
|
|
32
|
+
const value: History = {
|
|
33
|
+
[idx]: {
|
|
34
|
+
origin,
|
|
35
|
+
date: new Date(),
|
|
36
|
+
path,
|
|
37
|
+
metadata: this.config?.getMetadata?.(),
|
|
38
|
+
info,
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this._history = this.merge(this._history, value)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private merge(obj: object, addObj: AnyValue) {
|
|
46
|
+
return deepmerge({ all: true })(
|
|
47
|
+
obj ?? {},
|
|
48
|
+
addObj ?? {},
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private navigator: Navigator<O> = null
|
|
53
|
+
|
|
54
|
+
private routes: R = {} as R
|
|
55
|
+
|
|
56
|
+
constructor(
|
|
57
|
+
routes: R,
|
|
58
|
+
navigator: Navigator<O, C>,
|
|
59
|
+
config: Config = {},
|
|
60
|
+
) {
|
|
61
|
+
this.navigator = navigator
|
|
62
|
+
this.routes = routes
|
|
63
|
+
this.config = this.merge(this.config, config)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Checks if the user is on a certain route based on the parameters passed
|
|
68
|
+
* @param route Route that will be used to direct
|
|
69
|
+
* @param routeParams Parameters that will be applied to the route
|
|
70
|
+
* @param exact Accurate path checking - default false
|
|
71
|
+
* @returns Is on the route - boolean
|
|
72
|
+
*/
|
|
73
|
+
public isCurrentRoute<T extends keyof Routes<R>>(
|
|
74
|
+
route: T,
|
|
75
|
+
// @ts-expect-error
|
|
76
|
+
routeParams: Record<Routes<R>[T], string | number> = {} as any,
|
|
77
|
+
exact = false,
|
|
78
|
+
) {
|
|
79
|
+
if (IS_SSR) return false
|
|
80
|
+
|
|
81
|
+
let path = window?.location?.pathname
|
|
82
|
+
|
|
83
|
+
// @ts-ignore
|
|
84
|
+
const routePath = this.getPathWithParams(route, routeParams)
|
|
85
|
+
|
|
86
|
+
const isRootPath = routePath === '/'
|
|
87
|
+
|
|
88
|
+
if (isRootPath) {
|
|
89
|
+
const { pathname, origin, href } = window.location || {}
|
|
90
|
+
|
|
91
|
+
path = href?.replace(path, '')
|
|
92
|
+
path = path?.replace(origin, '')
|
|
93
|
+
|
|
94
|
+
return !path || pathname == routePath
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (exact) {
|
|
98
|
+
return path?.endsWith(routePath)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (path?.includes(routePath)) {
|
|
102
|
+
return true
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get the path from the route
|
|
110
|
+
* @param route Route that will be used to direct
|
|
111
|
+
* @returns Path - string
|
|
112
|
+
*/
|
|
113
|
+
public getPath(route: keyof Routes<R>): string {
|
|
114
|
+
let path = this.routes
|
|
115
|
+
|
|
116
|
+
// @ts-ignore
|
|
117
|
+
if (route?.includes('.')) {
|
|
118
|
+
// @ts-ignore
|
|
119
|
+
const indexesAccess = route?.split('.')
|
|
120
|
+
|
|
121
|
+
for (const index of indexesAccess) {
|
|
122
|
+
path = path?.[index]
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
// @ts-ignore
|
|
126
|
+
path = path?.[route]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return String(path)?.trim?.()
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get the path from the route and route params
|
|
134
|
+
* @param route Route that will be used to direct
|
|
135
|
+
* @param routeParams Parameters that will be applied to the route
|
|
136
|
+
* @returns Path - string
|
|
137
|
+
*/
|
|
138
|
+
public getPathWithParams<T extends keyof Routes<R>>(
|
|
139
|
+
route: T,
|
|
140
|
+
// @ts-expect-error
|
|
141
|
+
routeParams: Record<Routes<R>[T], string | number> = {} as any,
|
|
142
|
+
) {
|
|
143
|
+
let path = this.getPath(route)
|
|
144
|
+
|
|
145
|
+
for (const key in routeParams) {
|
|
146
|
+
const value = String(routeParams?.[key])
|
|
147
|
+
|
|
148
|
+
const searchPartial = `{{${key}}}`
|
|
149
|
+
|
|
150
|
+
if (path?.includes(searchPartial)) {
|
|
151
|
+
path = path?.replace(searchPartial, encodeURIComponent(value))
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!path?.startsWith('/')) {
|
|
156
|
+
path = '/' + path
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!path?.endsWith('/')) {
|
|
160
|
+
path = path + '/'
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return path?.trim()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Function to navigate to the previous page, if history is enabled, the penultimate record will be used,
|
|
168
|
+
* otherwise the browser's own api with "history.back()"
|
|
169
|
+
*/
|
|
170
|
+
public goBack() {
|
|
171
|
+
if (IS_SSR) return
|
|
172
|
+
|
|
173
|
+
if (!this.config.historyEnabled) {
|
|
174
|
+
history?.back?.()
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const lastIdx = Object.keys(this.history)?.length
|
|
179
|
+
|
|
180
|
+
const historyData = this.history[lastIdx - 1]
|
|
181
|
+
|
|
182
|
+
if (!historyData) {
|
|
183
|
+
throw new Error('Not find back route')
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const info = this.merge(historyData?.info, {
|
|
187
|
+
'action': 'goBack',
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
this.to(historyData?.path, historyData?.info?.options ?? {}, info)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Main function to navigate between the app using the route system with dynamic parameters
|
|
195
|
+
* @param route Route that will be used to direct
|
|
196
|
+
* @param options Route parameters (marked by {{}}), which will be automatically shown if they exist, and navigator options and route queryParams can also be passed through the "params" property
|
|
197
|
+
*/
|
|
198
|
+
public navigate<T extends keyof Routes<R>>(
|
|
199
|
+
route: T,
|
|
200
|
+
// @ts-expect-error
|
|
201
|
+
options: Record<Routes<R>[T], string | number> & O & { params?: RouteParams } = {} as any,
|
|
202
|
+
) {
|
|
203
|
+
// @ts-ignore
|
|
204
|
+
let path = this.getPath(route)
|
|
205
|
+
|
|
206
|
+
let _options = {}
|
|
207
|
+
let params = null
|
|
208
|
+
let routeParams = {}
|
|
209
|
+
|
|
210
|
+
const queryParamsKey = 'params'
|
|
211
|
+
|
|
212
|
+
for (const key in options) {
|
|
213
|
+
const value = options?.[key]
|
|
214
|
+
|
|
215
|
+
const searchPartial = `{{${key}}}`
|
|
216
|
+
|
|
217
|
+
if (path?.includes(searchPartial)) {
|
|
218
|
+
path = path?.replace(searchPartial, encodeURIComponent(String(value)))
|
|
219
|
+
|
|
220
|
+
routeParams = this.merge(routeParams, { [key]: String(value) })
|
|
221
|
+
} else if (key == queryParamsKey) {
|
|
222
|
+
params = value
|
|
223
|
+
} else {
|
|
224
|
+
_options = this.merge(_options, { [key]: value })
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!path?.startsWith('/')) {
|
|
229
|
+
path = '/' + path
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (typeof params === 'object') {
|
|
233
|
+
let searchParams = null
|
|
234
|
+
|
|
235
|
+
for (const paramKey in (params ?? {})) {
|
|
236
|
+
const value = params?.[paramKey]
|
|
237
|
+
const param = `${paramKey}=${encodeURIComponent(value)}`
|
|
238
|
+
const separator = searchParams == null ? '' : '&'
|
|
239
|
+
|
|
240
|
+
searchParams = `${searchParams ?? ''}${separator}${param}`
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (typeof searchParams === 'string') {
|
|
244
|
+
if (path?.endsWith('/')) {
|
|
245
|
+
path = path.slice(0, -1)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
path = `${path}?${searchParams}`
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!path?.endsWith('/') && !params) {
|
|
253
|
+
path = path + '/'
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
this.to(path?.trim(), _options as O, {
|
|
257
|
+
params,
|
|
258
|
+
routeParams,
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Calls the navigator to direct the user to a page
|
|
264
|
+
* @param path Path to which the user will be taken
|
|
265
|
+
* @param options Options that will be passed to the navigator
|
|
266
|
+
* @param info Information that will be added to the history
|
|
267
|
+
*/
|
|
268
|
+
public to(path: RoutePath, options: O = null as O, info = {}) {
|
|
269
|
+
if (this.config.historyEnabled) {
|
|
270
|
+
this.putHistory(path, this.merge(options, info))
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
this.navigator(path, options, this.context)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Clear history data
|
|
278
|
+
*/
|
|
279
|
+
public wipeHistory() {
|
|
280
|
+
this._history = {}
|
|
281
|
+
}
|
|
282
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type ExtractParams<T extends string> =
|
|
2
|
+
T extends `${infer _Start}{{${infer Param}}}${infer Rest}`
|
|
3
|
+
? Param | ExtractParams<Rest>
|
|
4
|
+
: never
|
|
5
|
+
|
|
6
|
+
type Params<T> = {
|
|
7
|
+
[K in keyof T]: T[K] extends string ? ExtractParams<T[K]> : Params<T[K]>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// @ts-ignore
|
|
11
|
+
type ExtractRoutes<T, PM, Prefix extends string = ''> = {
|
|
12
|
+
[K in keyof T & string]:
|
|
13
|
+
T[K] extends string
|
|
14
|
+
? {
|
|
15
|
+
// @ts-ignore
|
|
16
|
+
[P in `${Prefix}${Prefix extends '' ? '' : '.'}${K}`]: PM[K]
|
|
17
|
+
} // @ts-ignore
|
|
18
|
+
: ExtractRoutes<T[K], PM[K], `${Prefix}${Prefix extends '' ? '' : '.'}${K}`>
|
|
19
|
+
}[keyof T]
|
|
20
|
+
|
|
21
|
+
type UnionToIntersection<U> =
|
|
22
|
+
(U extends any ? (x: U) => void : never) extends ((x: infer I) => void) ? I : never
|
|
23
|
+
|
|
24
|
+
export type Routes<T> = UnionToIntersection<ExtractRoutes<T, Params<T>>>
|
|
25
|
+
|
|
26
|
+
// Navigation types
|
|
27
|
+
|
|
28
|
+
export type RoutePath = string
|
|
29
|
+
|
|
30
|
+
export type RouteParams = {
|
|
31
|
+
[x: string]: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type Navigator<O extends object = {}, C extends Record<string, any> = {}> = (path: RoutePath, options: O, context?: C) => void
|
|
35
|
+
|
|
36
|
+
export type AnyValue = {
|
|
37
|
+
[key: string]: any
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type HistoryData = {
|
|
41
|
+
origin: string
|
|
42
|
+
date: Date
|
|
43
|
+
path: RoutePath
|
|
44
|
+
metadata: any
|
|
45
|
+
info: any
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export type History = Record<number, HistoryData>
|
|
49
|
+
|
|
50
|
+
export type Config = {
|
|
51
|
+
historyEnabled?: boolean
|
|
52
|
+
getMetadata?: () => any
|
|
53
|
+
}
|