@inglorious/web 4.4.0 → 4.5.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/README.md +22 -221
- package/package.json +5 -28
- package/src/form/handlers.js +332 -0
- package/src/form/helpers.js +32 -0
- package/src/form/index.js +4 -373
- package/src/index.js +1 -0
- package/src/router/handlers.js +345 -0
- package/src/router/helpers.js +46 -0
- package/src/router/index.js +5 -384
- package/src/test.js +1 -1
- package/types/index.d.ts +0 -3
- package/src/list/index.js +0 -7
- package/src/list/logic.js +0 -75
- package/src/list/rendering.js +0 -105
- package/src/select/base.css +0 -52
- package/src/select/index.js +0 -7
- package/src/select/logic.js +0 -332
- package/src/select/rendering.js +0 -352
- package/src/select/theme.css +0 -133
- package/src/table/base.css +0 -34
- package/src/table/filters/date.js +0 -42
- package/src/table/filters/number.js +0 -22
- package/src/table/filters/range.js +0 -40
- package/src/table/filters/select.js +0 -34
- package/src/table/filters/text.js +0 -22
- package/src/table/filters.js +0 -24
- package/src/table/index.js +0 -7
- package/src/table/logic.js +0 -573
- package/src/table/rendering.js +0 -340
- package/src/table/theme.css +0 -83
- package/types/list.d.ts +0 -91
- package/types/select.d.ts +0 -136
- package/types/table.d.ts +0 -248
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import("../../types/router").RouterType} RouterType
|
|
3
|
+
* @typedef {import("../../types/router").RouterEntity} RouterEntity
|
|
4
|
+
* @typedef {import("../../types/router").Api} Api
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const SKIP_FULL_MATCH_GROUP = 1 // .match() result at index 0 is the full string
|
|
8
|
+
const REMOVE_COLON_PREFIX = 1
|
|
9
|
+
|
|
10
|
+
export function createHandlers(routeConfig) {
|
|
11
|
+
/**
|
|
12
|
+
* Guard to ensure global listeners are only attached once.
|
|
13
|
+
* @type {boolean}
|
|
14
|
+
*/
|
|
15
|
+
let areListenersInitialized = false
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
/**
|
|
19
|
+
* Initializes the router entity.
|
|
20
|
+
* Sets up the initial route and event listeners for navigation.
|
|
21
|
+
* @param {RouterEntity} entity - The router entity.
|
|
22
|
+
* @param {void} payload - Unused payload.
|
|
23
|
+
* @param {Api} api - The application API.
|
|
24
|
+
*/
|
|
25
|
+
init(entity, payload, api) {
|
|
26
|
+
// Handle initial route
|
|
27
|
+
const { pathname, search } = window.location
|
|
28
|
+
const initialPath = pathname + search
|
|
29
|
+
const route = findRoute(routeConfig, initialPath)
|
|
30
|
+
const entityId = entity.id
|
|
31
|
+
|
|
32
|
+
if (route) {
|
|
33
|
+
api.notify(`#${entityId}:navigate`, {
|
|
34
|
+
to: initialPath,
|
|
35
|
+
params: route.params,
|
|
36
|
+
replace: true,
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (areListenersInitialized) return
|
|
41
|
+
areListenersInitialized = true
|
|
42
|
+
|
|
43
|
+
// Listen for browser back/forward
|
|
44
|
+
window.addEventListener("popstate", () =>
|
|
45
|
+
api.notify(`#${entityId}:popstate`, payload),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
// Intercept link clicks
|
|
49
|
+
document.addEventListener("click", (event) => {
|
|
50
|
+
// Find the closest <a> tag (handles clicks on children)
|
|
51
|
+
const link = event.target.closest("a")
|
|
52
|
+
|
|
53
|
+
if (!link) return
|
|
54
|
+
|
|
55
|
+
// Skip external links
|
|
56
|
+
if (link.target === "_blank") return
|
|
57
|
+
if (link.rel === "external") return
|
|
58
|
+
if (link.hasAttribute("data-external")) return
|
|
59
|
+
|
|
60
|
+
// Skip different origin links
|
|
61
|
+
if (link.origin !== window.location.origin) return
|
|
62
|
+
|
|
63
|
+
// Skip links with download attribute
|
|
64
|
+
if (link.hasAttribute("download")) return
|
|
65
|
+
|
|
66
|
+
// Skip mailto:, tel:, etc.
|
|
67
|
+
if (!["http:", "https:"].includes(link.protocol)) return
|
|
68
|
+
|
|
69
|
+
// Prevent default and use router
|
|
70
|
+
event.preventDefault()
|
|
71
|
+
|
|
72
|
+
const path = link.pathname + link.search + link.hash
|
|
73
|
+
api.notify(`#${entityId}:navigate`, path)
|
|
74
|
+
})
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
create(entity) {
|
|
78
|
+
entity.path ??= "/"
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Handles browser `popstate` events.
|
|
83
|
+
* Attempts to match the current location to a route and updates the router
|
|
84
|
+
* entity state. If the matched route is lazy (a loader function) it will
|
|
85
|
+
* attempt to load the module first.
|
|
86
|
+
* @param {RouterEntity} entity
|
|
87
|
+
* @param {Object} payload
|
|
88
|
+
* @param {Api} api
|
|
89
|
+
*/
|
|
90
|
+
async popstate(entity, payload, api) {
|
|
91
|
+
const path = window.location.pathname + window.location.search
|
|
92
|
+
const route = findRoute(routeConfig, path)
|
|
93
|
+
const entityId = entity.id
|
|
94
|
+
|
|
95
|
+
if (route) {
|
|
96
|
+
if (typeof route.entityType === "function") {
|
|
97
|
+
entity.isLoading = true
|
|
98
|
+
entity.error = null
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const module = await route.entityType()
|
|
102
|
+
api.notify(`#${entityId}:routeLoadSuccess`, { module, route })
|
|
103
|
+
api.notify(`#${entityId}:popstate`, payload)
|
|
104
|
+
} catch (error) {
|
|
105
|
+
api.notify(`#${entityId}:routeLoadError`, { error, path })
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
updateRouter(entity, route)
|
|
112
|
+
|
|
113
|
+
api.notify("routeChange", {
|
|
114
|
+
route: entity.route,
|
|
115
|
+
params: entity.params,
|
|
116
|
+
query: entity.query,
|
|
117
|
+
path: entity.path,
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Handles navigation to a new route.
|
|
124
|
+
* @param {RouterEntity} entity - The router entity.
|
|
125
|
+
* @param {string|number|{to: string, params?: object, replace?: boolean, state?: object}} payload - The navigation target or options.
|
|
126
|
+
* @param {Api} api - The application API.
|
|
127
|
+
*/
|
|
128
|
+
async navigate(entity, payload, api) {
|
|
129
|
+
const options = ["number", "string"].includes(typeof payload)
|
|
130
|
+
? { to: payload }
|
|
131
|
+
: payload
|
|
132
|
+
const { to, params, replace, force, state = {} } = options
|
|
133
|
+
|
|
134
|
+
// Numeric navigation (back/forward)
|
|
135
|
+
if (typeof to === "number") {
|
|
136
|
+
history.go(to)
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Build final path
|
|
141
|
+
let path = to
|
|
142
|
+
|
|
143
|
+
// If params provided and "to" looks like a pattern, build the path
|
|
144
|
+
if (params && to.includes(":")) {
|
|
145
|
+
path = buildPath(to, params)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// If "to" is already a final path (like "/users/1"), use it directly
|
|
149
|
+
// The router will match it against patterns in routeConfig
|
|
150
|
+
|
|
151
|
+
const route = findRoute(routeConfig, path)
|
|
152
|
+
const entityId = entity.id
|
|
153
|
+
|
|
154
|
+
if (!route) {
|
|
155
|
+
console.warn(`No route matches path: ${path}`)
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!force) {
|
|
160
|
+
// Prevent navigation if the full path (including query/hash) is identical.
|
|
161
|
+
const currentFullPath =
|
|
162
|
+
entity.path + window.location.search + window.location.hash
|
|
163
|
+
if (path === currentFullPath) {
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Asynchronous navigation
|
|
169
|
+
if (typeof route.entityType === "function") {
|
|
170
|
+
entity.isLoading = true
|
|
171
|
+
entity.error = null
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const module = await route.entityType()
|
|
175
|
+
api.notify(`#${entityId}:routeLoadSuccess`, { module, route })
|
|
176
|
+
api.notify(`#${entityId}:navigate`, payload)
|
|
177
|
+
} catch (error) {
|
|
178
|
+
api.notify(`#${entityId}:routeLoadError`, { error, path })
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
updateRouter(entity, route)
|
|
185
|
+
|
|
186
|
+
// Prepare history state
|
|
187
|
+
const historyState = {
|
|
188
|
+
...state,
|
|
189
|
+
route: entity.route,
|
|
190
|
+
params: entity.params,
|
|
191
|
+
query: entity.query,
|
|
192
|
+
path: entity.path,
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Navigate
|
|
196
|
+
const method = replace ? "replaceState" : "pushState"
|
|
197
|
+
history[method](historyState, "", path)
|
|
198
|
+
|
|
199
|
+
api.notify("routeChange", historyState)
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Handles successful loading of a lazily-loaded route module.
|
|
204
|
+
* Registers the loaded type in the runtime type registry via `api.setType`
|
|
205
|
+
* and updates the `routeConfig` entry for the pattern.
|
|
206
|
+
* @param {RouterEntity} entity
|
|
207
|
+
* @param {{module: object, route: {pattern: string, entityType: string}}} payload
|
|
208
|
+
* @param {Api} api
|
|
209
|
+
*/
|
|
210
|
+
routeLoadSuccess(entity, payload, api) {
|
|
211
|
+
const { module, route } = payload
|
|
212
|
+
|
|
213
|
+
const [typeName, type] = Object.entries(module).find(
|
|
214
|
+
([, type]) => type?.render,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
api.setType(typeName, type)
|
|
218
|
+
routeConfig[route.pattern] = typeName
|
|
219
|
+
|
|
220
|
+
entity.isLoading = false
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Handles errors during lazy route loading.
|
|
225
|
+
* @param {RouterEntity} entity - The router entity.
|
|
226
|
+
* @param {{error: Error, path: string}} payload - The error payload.
|
|
227
|
+
*/
|
|
228
|
+
routeLoadError(entity, payload) {
|
|
229
|
+
const { error, path } = payload
|
|
230
|
+
console.error(`Failed to load route ${path}:`, error)
|
|
231
|
+
entity.path = path
|
|
232
|
+
entity.isLoading = false
|
|
233
|
+
entity.error = error
|
|
234
|
+
},
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Builds a URL path by substituting parameters into a route pattern.
|
|
240
|
+
* @param {string} pattern - The route pattern (e.g., "/users/:userId").
|
|
241
|
+
* @param {Record<string, string>} [params={}] - The parameters to substitute.
|
|
242
|
+
* @returns {string} The constructed path.
|
|
243
|
+
* @example
|
|
244
|
+
* buildPath("/users/:userId", { userId: "123" }) // returns "/users/123"
|
|
245
|
+
*/
|
|
246
|
+
function buildPath(pattern, params = {}) {
|
|
247
|
+
let path = pattern
|
|
248
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
249
|
+
path = path.replace(`:${key}`, value)
|
|
250
|
+
})
|
|
251
|
+
return path
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Finds a matching route configuration for a given URL path.
|
|
256
|
+
* It supports parameterized routes and a fallback "*" route.
|
|
257
|
+
* @param {Record<string, string>} routeConfig - The routes configuration map.
|
|
258
|
+
* @param {string} pathname - The URL path to match.
|
|
259
|
+
* @returns {{pattern: string, entityType: string, params: Record<string, string>, path: string}|null}
|
|
260
|
+
* The matched route object or null if no match is found.
|
|
261
|
+
*/
|
|
262
|
+
function findRoute(routeConfig, pathname) {
|
|
263
|
+
const [path, search] = pathname.split("?")
|
|
264
|
+
let fallbackRoute = null
|
|
265
|
+
|
|
266
|
+
for (const [pattern, entityType] of Object.entries(routeConfig)) {
|
|
267
|
+
if (pattern === "*") {
|
|
268
|
+
fallbackRoute = { pattern, entityType, params: {}, path }
|
|
269
|
+
continue
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const params = matchRoute(pattern, path)
|
|
273
|
+
if (params !== null) {
|
|
274
|
+
const query = search
|
|
275
|
+
? Object.fromEntries(new URLSearchParams(search))
|
|
276
|
+
: {}
|
|
277
|
+
return { pattern, entityType, params, path, query }
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return fallbackRoute
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Matches a URL path against a route pattern and extracts any parameters.
|
|
286
|
+
* @param {string} pattern - The route pattern (e.g., "/users/:userId").
|
|
287
|
+
* @param {string} path - The URL path to match (e.g., "/users/123").
|
|
288
|
+
* @returns {Record<string, string>|null} An object of extracted parameters,
|
|
289
|
+
* or null if the path does not match the pattern.
|
|
290
|
+
*/
|
|
291
|
+
function matchRoute(pattern, path) {
|
|
292
|
+
const paramNames = getParamNames(pattern)
|
|
293
|
+
const regex = patternToRegex(pattern)
|
|
294
|
+
const match = path.match(regex)
|
|
295
|
+
|
|
296
|
+
if (!match) return null
|
|
297
|
+
|
|
298
|
+
const params = {}
|
|
299
|
+
paramNames.forEach((name, i) => {
|
|
300
|
+
params[name] = match[i + SKIP_FULL_MATCH_GROUP]
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
return params
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Parses a route pattern and extracts the names of its parameters.
|
|
308
|
+
* @param {string} pattern - The route pattern.
|
|
309
|
+
* @returns {string[]} An array of parameter names.
|
|
310
|
+
* @example
|
|
311
|
+
* getParamNames("/users/:userId/posts/:postId") // returns ["userId", "postId"]
|
|
312
|
+
*/
|
|
313
|
+
function getParamNames(pattern) {
|
|
314
|
+
const matches = pattern.match(/:(\w+)/g)
|
|
315
|
+
return matches ? matches.map((m) => m.slice(REMOVE_COLON_PREFIX)) : []
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Converts a route pattern into a regular expression for matching URL paths.
|
|
320
|
+
* @param {string} pattern - The route pattern (e.g., "/users/:userId").
|
|
321
|
+
* @returns {RegExp} The corresponding regular expression.
|
|
322
|
+
*/
|
|
323
|
+
function patternToRegex(pattern) {
|
|
324
|
+
const regexPattern = pattern
|
|
325
|
+
.replace(/\//g, "\\/")
|
|
326
|
+
.replace(/:(\w+)/g, "([^\\/]+)")
|
|
327
|
+
return new RegExp(`^${regexPattern}$`)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Updates the router entity's internal state.
|
|
332
|
+
* @param {RouterEntity} entity - The router entity.
|
|
333
|
+
* @param {object} options - The update options.
|
|
334
|
+
* @param {string} options.entityType - The matched entity type.
|
|
335
|
+
* @param {string} options.path - The full path (pathname only, no query).
|
|
336
|
+
* @param {object} options.params - The extracted route parameters.
|
|
337
|
+
* @param {object} [options.query] - The parsed query parameters.
|
|
338
|
+
*/
|
|
339
|
+
function updateRouter(entity, { entityType, path, params, query }) {
|
|
340
|
+
entity.route = entityType
|
|
341
|
+
entity.path = path
|
|
342
|
+
entity.params = params
|
|
343
|
+
entity.query = query
|
|
344
|
+
entity.hash = window.location.hash
|
|
345
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export function createHelpers(routeConfig) {
|
|
2
|
+
return {
|
|
3
|
+
/**
|
|
4
|
+
* Retrieves the current route configuration.
|
|
5
|
+
* @returns {Record<string, string|function>} The current route configuration.
|
|
6
|
+
*/
|
|
7
|
+
getRoutes() {
|
|
8
|
+
return routeConfig
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Retrieves a single route configuration given its path.
|
|
13
|
+
* @param {string} path - The path of the route to retrieve.
|
|
14
|
+
* @returns {string|function|undefined} The route configuration or undefined if not found.
|
|
15
|
+
*/
|
|
16
|
+
getRoute(path) {
|
|
17
|
+
return routeConfig[path]
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Sets or updates routes in the route configuration.
|
|
22
|
+
* Can be used both during initialization and at any point to add or update routes dynamically.
|
|
23
|
+
* @param {Record<string, string|function>} routes - An object mapping route paths/patterns to entity type names or loader functions.
|
|
24
|
+
*/
|
|
25
|
+
setRoutes(routes) {
|
|
26
|
+
Object.assign(routeConfig, routes)
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Adds a single route to the route configuration.
|
|
31
|
+
* @param {string} path - The route path or pattern (e.g., "/users/:userId").
|
|
32
|
+
* @param {string|function} route - The entity type name or a function that dynamically loads it.
|
|
33
|
+
*/
|
|
34
|
+
addRoute(path, route) {
|
|
35
|
+
routeConfig[path] = route
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Removes a route from the route configuration.
|
|
40
|
+
* @param {string} path - The route path or pattern to remove.
|
|
41
|
+
*/
|
|
42
|
+
removeRoute(path) {
|
|
43
|
+
delete routeConfig[path]
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
}
|