@lightningjs/blits 2.3.0 → 2.4.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/CHANGELOG.md +13 -0
- package/index.d.ts +13 -2
- package/package.json +1 -1
- package/src/component/base/router.js +136 -22
- package/src/components/RouterView.js +27 -7
- package/src/focus/focus.js +4 -1
- package/src/lib/reactivityguard/computedprops.js +235 -153
- package/src/router/router.js +98 -48
- package/src/router/utils.js +96 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v2.4.1
|
|
4
|
+
|
|
5
|
+
_02 jun 2026_
|
|
6
|
+
|
|
7
|
+
- Fixed router issue with initial redirect
|
|
8
|
+
|
|
9
|
+
## v2.4.0
|
|
10
|
+
|
|
11
|
+
_1 june 2026_
|
|
12
|
+
|
|
13
|
+
- Improved correct registration of reactivity in computed props
|
|
14
|
+
- Added named router views to enable multiple nested router views
|
|
15
|
+
|
|
3
16
|
## v2.3.0
|
|
4
17
|
|
|
5
18
|
_20 may 2026_
|
package/index.d.ts
CHANGED
|
@@ -329,7 +329,14 @@ declare module '@lightningjs/blits' {
|
|
|
329
329
|
*
|
|
330
330
|
* @param {string}
|
|
331
331
|
*/
|
|
332
|
-
to(location: string, data?: RouteData, options?: RouteOptions):
|
|
332
|
+
to(location: string, data?: RouteData, options?: RouteOptions): boolean;
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Get a router scoped to a named RouterView
|
|
336
|
+
*
|
|
337
|
+
* @param {string}
|
|
338
|
+
*/
|
|
339
|
+
get(name: string): Router;
|
|
333
340
|
|
|
334
341
|
/**
|
|
335
342
|
* Navigate to the previous location
|
|
@@ -707,6 +714,10 @@ declare module '@lightningjs/blits' {
|
|
|
707
714
|
* Watchers for changes to state variables, props or computed properties
|
|
708
715
|
*/
|
|
709
716
|
watch?: W & ComponentContext<P, S, M, C>
|
|
717
|
+
/**
|
|
718
|
+
* Router Configuration
|
|
719
|
+
*/
|
|
720
|
+
router?: RouterConfig<P, S, M, C>
|
|
710
721
|
}
|
|
711
722
|
|
|
712
723
|
export interface RouterHooks {
|
|
@@ -759,7 +770,7 @@ declare module '@lightningjs/blits' {
|
|
|
759
770
|
|
|
760
771
|
export type ApplicationConfig<P extends Props, S, M, C, W> = Omit<
|
|
761
772
|
ComponentConfig<P, S, M, C, W>,
|
|
762
|
-
'hooks' | 'methods' | 'input' | 'computed' | 'watch' | 'state'
|
|
773
|
+
'hooks' | 'methods' | 'input' | 'computed' | 'watch' | 'state' | 'router' | 'routes'
|
|
763
774
|
> & {
|
|
764
775
|
hooks?: Hooks & ApplicationContext<P, S, M, C>
|
|
765
776
|
methods?: M & ApplicationContext<P, S, M, C>
|
package/package.json
CHANGED
|
@@ -16,31 +16,145 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import symbols from '../../lib/symbols.js'
|
|
19
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
back,
|
|
21
|
+
currentRoute,
|
|
22
|
+
getRegisteredRouterView,
|
|
23
|
+
getSingleRegisteredRouterView,
|
|
24
|
+
state,
|
|
25
|
+
toRouterView,
|
|
26
|
+
} from '../../router/router.js'
|
|
27
|
+
|
|
28
|
+
const routerCache = new WeakMap()
|
|
29
|
+
|
|
30
|
+
const isRouterView = (component, routerViewName = '') => {
|
|
31
|
+
if (component === undefined || component === null) return false
|
|
32
|
+
return (
|
|
33
|
+
Array.isArray(component.history) && (routerViewName === '' || component.name === routerViewName)
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const getRouterViewInChildren = (component, routerViewName = '') => {
|
|
38
|
+
if (component === undefined || component === null) return null
|
|
39
|
+
|
|
40
|
+
const children = component[symbols.children]
|
|
41
|
+
if (Array.isArray(children) === false) return null
|
|
42
|
+
|
|
43
|
+
for (let i = 0; i < children.length; i++) {
|
|
44
|
+
const child = children[i]
|
|
45
|
+
if (isRouterView(child, routerViewName)) return child
|
|
46
|
+
|
|
47
|
+
const nestedRouterView = getRouterViewInChildren(child, routerViewName)
|
|
48
|
+
if (nestedRouterView !== null) return nestedRouterView
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const getRouterView = (component, routerViewName = '') => {
|
|
55
|
+
let current = component
|
|
56
|
+
while (current !== undefined && current !== null) {
|
|
57
|
+
if (isRouterView(current, routerViewName)) {
|
|
58
|
+
return current
|
|
59
|
+
}
|
|
60
|
+
current = current[symbols.parent]
|
|
61
|
+
}
|
|
62
|
+
return getRouterViewInChildren(component, routerViewName)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const resolveRouterView = (component, routerViewName) => {
|
|
66
|
+
if (routerViewName !== undefined) {
|
|
67
|
+
const registeredRouterView = getRegisteredRouterView(routerViewName)
|
|
68
|
+
if (registeredRouterView !== null) return registeredRouterView
|
|
69
|
+
|
|
70
|
+
return getRouterView(component, routerViewName)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const singleRouterView = getSingleRegisteredRouterView()
|
|
74
|
+
if (singleRouterView !== null) return singleRouterView
|
|
75
|
+
|
|
76
|
+
return getRouterView(component)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const getRoutes = (component, routerViewName) => {
|
|
80
|
+
const routerView = resolveRouterView(component, routerViewName)
|
|
81
|
+
if (
|
|
82
|
+
routerView !== null &&
|
|
83
|
+
routerView[symbols.parent] !== undefined &&
|
|
84
|
+
routerView[symbols.parent][symbols.routes] !== undefined
|
|
85
|
+
) {
|
|
86
|
+
return routerView[symbols.parent][symbols.routes]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (component[symbols.routes] !== undefined) return component[symbols.routes]
|
|
90
|
+
|
|
91
|
+
if (
|
|
92
|
+
component[symbols.parent] !== undefined &&
|
|
93
|
+
component[symbols.parent][symbols.routes] !== undefined
|
|
94
|
+
) {
|
|
95
|
+
return component[symbols.parent][symbols.routes]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return undefined
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const getRouterFromCache = (component, routerViewName) => {
|
|
102
|
+
let routers = routerCache.get(component)
|
|
103
|
+
if (routers === undefined) {
|
|
104
|
+
routers = new Map()
|
|
105
|
+
routerCache.set(component, routers)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const cacheKey = routerViewName === undefined ? '' : routerViewName
|
|
109
|
+
const router = routers.get(cacheKey)
|
|
110
|
+
if (router !== undefined) return router
|
|
111
|
+
|
|
112
|
+
const newRouter = createRouter(component, routerViewName)
|
|
113
|
+
routers.set(cacheKey, newRouter)
|
|
114
|
+
return newRouter
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const createRouter = (component, routerViewName) => {
|
|
118
|
+
return {
|
|
119
|
+
to(path, data = {}, options = {}) {
|
|
120
|
+
const routerView = resolveRouterView(component, routerViewName)
|
|
121
|
+
if (routerView === null) return false
|
|
122
|
+
|
|
123
|
+
toRouterView(routerView, path, data, options)
|
|
124
|
+
return true
|
|
125
|
+
},
|
|
126
|
+
back() {
|
|
127
|
+
const routerView = resolveRouterView(component, routerViewName)
|
|
128
|
+
return routerView === null ? false : back.call(routerView)
|
|
129
|
+
},
|
|
130
|
+
get(name) {
|
|
131
|
+
return getRouterFromCache(component, name)
|
|
132
|
+
},
|
|
133
|
+
get backNavigation() {
|
|
134
|
+
return state.backNavigation !== false
|
|
135
|
+
},
|
|
136
|
+
set backNavigation(enabled) {
|
|
137
|
+
state.backNavigation = enabled !== false
|
|
138
|
+
},
|
|
139
|
+
get currentRoute() {
|
|
140
|
+
const routerView = resolveRouterView(component, routerViewName)
|
|
141
|
+
return routerView === null ? currentRoute : routerView.currentRoute
|
|
142
|
+
},
|
|
143
|
+
get routes() {
|
|
144
|
+
return getRoutes(component, routerViewName)
|
|
145
|
+
},
|
|
146
|
+
get navigating() {
|
|
147
|
+
return state.navigating
|
|
148
|
+
},
|
|
149
|
+
state,
|
|
150
|
+
}
|
|
151
|
+
}
|
|
20
152
|
|
|
21
153
|
export default {
|
|
22
154
|
$router: {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
get backNavigation() {
|
|
27
|
-
return state.backNavigation !== false
|
|
28
|
-
},
|
|
29
|
-
set backNavigation(enabled) {
|
|
30
|
-
state.backNavigation = enabled !== false
|
|
31
|
-
},
|
|
32
|
-
get currentRoute() {
|
|
33
|
-
return currentRoute
|
|
34
|
-
},
|
|
35
|
-
get routes() {
|
|
36
|
-
return this[symbols.routes]
|
|
37
|
-
},
|
|
38
|
-
get navigating() {
|
|
39
|
-
return state.navigating
|
|
40
|
-
},
|
|
41
|
-
state,
|
|
42
|
-
},
|
|
43
|
-
writable: false,
|
|
155
|
+
get() {
|
|
156
|
+
return getRouterFromCache(this)
|
|
157
|
+
},
|
|
44
158
|
enumerable: true,
|
|
45
159
|
configurable: false,
|
|
46
160
|
},
|
|
@@ -16,12 +16,14 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import Component from '../component.js'
|
|
19
|
-
import Router, {
|
|
19
|
+
import Router, {
|
|
20
|
+
registerRouterView,
|
|
21
|
+
state as routerState,
|
|
22
|
+
unregisterRouterView,
|
|
23
|
+
} from '../router/router.js'
|
|
20
24
|
import symbols from '../lib/symbols.js'
|
|
21
25
|
import Focus from '../focus/focus.js'
|
|
22
26
|
|
|
23
|
-
let hashchangeHandler = null
|
|
24
|
-
|
|
25
27
|
/** @typedef {{ $input?: (event: any) => boolean, $focus?: (event: any) => void }} RouterViewParent */
|
|
26
28
|
|
|
27
29
|
export default () =>
|
|
@@ -123,24 +125,42 @@ export default () =>
|
|
|
123
125
|
effects: [],
|
|
124
126
|
context: {},
|
|
125
127
|
},
|
|
128
|
+
props: {
|
|
129
|
+
name: '',
|
|
130
|
+
active: true,
|
|
131
|
+
},
|
|
126
132
|
state() {
|
|
127
133
|
return {
|
|
128
134
|
activeView: null,
|
|
135
|
+
history: [],
|
|
129
136
|
}
|
|
130
137
|
},
|
|
138
|
+
watch: {
|
|
139
|
+
active(v, old) {
|
|
140
|
+
if (v === true && old === false) {
|
|
141
|
+
Router.navigate.apply(this)
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
},
|
|
131
145
|
hooks: {
|
|
132
146
|
async ready() {
|
|
147
|
+
registerRouterView(this)
|
|
148
|
+
|
|
133
149
|
const parent = this[symbols.parent]
|
|
134
150
|
if (parent && parent[symbols.routerHooks] && parent[symbols.routerHooks].init) {
|
|
135
151
|
await parent[symbols.routerHooks].init.apply(parent)
|
|
136
152
|
}
|
|
153
|
+
this.hashchangeHandler = () => Router.navigate.apply(this)
|
|
154
|
+
|
|
155
|
+
if (this.active === true) {
|
|
156
|
+
Router.navigate.apply(this)
|
|
157
|
+
}
|
|
137
158
|
|
|
138
|
-
|
|
139
|
-
Router.navigate.apply(this)
|
|
140
|
-
window.addEventListener('hashchange', hashchangeHandler)
|
|
159
|
+
window.addEventListener('hashchange', this.hashchangeHandler)
|
|
141
160
|
},
|
|
142
161
|
destroy() {
|
|
143
|
-
|
|
162
|
+
unregisterRouterView(this)
|
|
163
|
+
window.removeEventListener('hashchange', this.hashchangeHandler, false)
|
|
144
164
|
},
|
|
145
165
|
focus() {
|
|
146
166
|
if (this.activeView && Focus.get() === this) {
|
package/src/focus/focus.js
CHANGED
|
@@ -41,6 +41,8 @@ export default {
|
|
|
41
41
|
return focusedComponent
|
|
42
42
|
},
|
|
43
43
|
set(component, event) {
|
|
44
|
+
if (component === undefined || component.eol === true) return
|
|
45
|
+
|
|
44
46
|
clearTimeout(setFocusTimeout)
|
|
45
47
|
|
|
46
48
|
// early return if already focused
|
|
@@ -75,12 +77,13 @@ export default {
|
|
|
75
77
|
|
|
76
78
|
// and finally set focus to the leaf component
|
|
77
79
|
setFocusTimeout = setTimeout(
|
|
78
|
-
() => setFocus(component, event),
|
|
80
|
+
() => component.eol !== true && setFocus(component, event),
|
|
79
81
|
this.hold === true ? Settings.get('holdTimeout', DEFAULT_HOLD_TIMEOUT_MS) : 0
|
|
80
82
|
)
|
|
81
83
|
},
|
|
82
84
|
input(key, event) {
|
|
83
85
|
if (state.navigating === true) return
|
|
86
|
+
if (focusedComponent === null) return
|
|
84
87
|
|
|
85
88
|
const componentWithInputEvent = getComponentWithInputEvent(focusedComponent, key)
|
|
86
89
|
|
|
@@ -100,176 +100,258 @@ const skipNonCode = (text, pos) => {
|
|
|
100
100
|
return p
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
|
|
103
|
+
// Skip a non-method computed member (`key: value`) and return the position after it.
|
|
104
|
+
const skipComputedMember = (text, pos) => {
|
|
105
|
+
const keyMatch = /^(\w+)\s*:\s*/.exec(text.substring(pos))
|
|
106
|
+
if (!keyMatch) return -1
|
|
107
|
+
|
|
108
|
+
let p = pos + keyMatch[0].length
|
|
109
|
+
p = skipNonCode(text, p)
|
|
110
|
+
if (p >= text.length - 1) return p
|
|
111
|
+
|
|
112
|
+
if (text.startsWith('function', p)) {
|
|
113
|
+
const open = text.indexOf('{', p)
|
|
114
|
+
if (open === -1) return -1
|
|
115
|
+
const close = findMatchingBrace(text, open)
|
|
116
|
+
return close === -1 ? -1 : close
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let depth = 0
|
|
120
|
+
let inString = false
|
|
121
|
+
let stringChar = ''
|
|
122
|
+
|
|
123
|
+
for (; p < text.length - 1; p++) {
|
|
124
|
+
const char = text[p]
|
|
125
|
+
|
|
126
|
+
if (!inString && (char === '"' || char === "'" || char === '`')) {
|
|
127
|
+
inString = true
|
|
128
|
+
stringChar = char
|
|
129
|
+
} else if (inString && char === stringChar && text[p - 1] !== '\\') {
|
|
130
|
+
inString = false
|
|
131
|
+
} else if (!inString && char === '/' && text[p + 1] === '/') {
|
|
132
|
+
const nextNewline = text.indexOf('\n', p + 2)
|
|
133
|
+
p = nextNewline === -1 ? text.length - 2 : nextNewline
|
|
134
|
+
} else if (!inString && char === '/' && text[p + 1] === '*') {
|
|
135
|
+
const end = text.indexOf('*/', p + 2)
|
|
136
|
+
p = end === -1 ? text.length - 2 : end + 1
|
|
137
|
+
} else if (!inString) {
|
|
138
|
+
if (char === '{' || char === '(' || char === '[') depth++
|
|
139
|
+
else if (char === '}' || char === ')' || char === ']') depth--
|
|
140
|
+
else if (char === ',' && depth === 0) return p
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return p
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Find the opening `{` of the component config object after Blits.Component( / Application(
|
|
148
|
+
const findConfigBraceStart = (text, openParenPos) => {
|
|
149
|
+
let p = openParenPos + 1
|
|
150
|
+
p = skipNonCode(text, p)
|
|
151
|
+
if (p >= text.length) return -1
|
|
152
|
+
|
|
153
|
+
if (text[p] === '{') return p
|
|
154
|
+
|
|
155
|
+
if (text[p] === "'" || text[p] === '"' || text[p] === '`') {
|
|
156
|
+
const quote = text[p]
|
|
157
|
+
p++
|
|
158
|
+
while (p < text.length) {
|
|
159
|
+
if (text[p] === quote && text[p - 1] !== '\\') {
|
|
160
|
+
p++
|
|
161
|
+
break
|
|
162
|
+
}
|
|
163
|
+
p++
|
|
164
|
+
}
|
|
165
|
+
p = skipNonCode(text, p)
|
|
166
|
+
if (p < text.length && text[p] === '{') return p
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return -1
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const processComputedBlock = (configObject) => {
|
|
107
173
|
const modifications = []
|
|
108
174
|
const commentText = ' /* auto-generated reactivity guard */ '
|
|
109
175
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (!functionNameMatch) break
|
|
145
|
-
|
|
146
|
-
const funcName = functionNameMatch[1]
|
|
147
|
-
const funcNamePos = currentPos
|
|
148
|
-
|
|
149
|
-
// Find opening brace for this function
|
|
150
|
-
const afterSignaturePos = funcNamePos + functionNameMatch[0].length
|
|
151
|
-
const openingBracePos = computedObj.indexOf('{', afterSignaturePos)
|
|
152
|
-
if (openingBracePos === -1) break
|
|
153
|
-
|
|
154
|
-
// Find closing brace for individual computed function
|
|
155
|
-
const closingBracePos = findComputedFunctionEnd(computedObj, openingBracePos)
|
|
156
|
-
|
|
157
|
-
if (closingBracePos === -1) {
|
|
158
|
-
break
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Extract the entire function and its body
|
|
162
|
-
// This is better and more accurate than using regex to avoid nested braces
|
|
163
|
-
const functionStartPos = funcNamePos
|
|
164
|
-
const functionEndPos = closingBracePos
|
|
165
|
-
const fullFunction = computedObj.substring(functionStartPos, functionEndPos)
|
|
166
|
-
const functionBody = computedObj
|
|
167
|
-
.substring(openingBracePos + 1, closingBracePos - 1)
|
|
168
|
-
.trim()
|
|
169
|
-
|
|
170
|
-
computedProps.push({
|
|
171
|
-
name: funcName,
|
|
172
|
-
fullText: fullFunction,
|
|
173
|
-
body: functionBody,
|
|
174
|
-
startPos: functionStartPos,
|
|
175
|
-
endPos: functionEndPos,
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
// Move past this function
|
|
179
|
-
currentPos = closingBracePos
|
|
176
|
+
const computedKeywordMatch = /computed\s*:\s*{/.exec(configObject)
|
|
177
|
+
|
|
178
|
+
if (computedKeywordMatch) {
|
|
179
|
+
// Find start position of the computed block
|
|
180
|
+
const computedStart = computedKeywordMatch.index + computedKeywordMatch[0].length - 1 // Position of the opening brace
|
|
181
|
+
|
|
182
|
+
// Find the matching closing brace for computed object in config
|
|
183
|
+
const computedEnd = findComputedObjectEnd(configObject, computedStart)
|
|
184
|
+
|
|
185
|
+
if (computedEnd !== -1) {
|
|
186
|
+
const computedObj = configObject.substring(computedStart, computedEnd)
|
|
187
|
+
const computedBlockStart = computedKeywordMatch.index
|
|
188
|
+
const computedBlockEnd = computedEnd
|
|
189
|
+
|
|
190
|
+
// Process each computed property
|
|
191
|
+
const computedProps = []
|
|
192
|
+
let currentPos = 1 // Start after the opening brace
|
|
193
|
+
|
|
194
|
+
// Process computed properties content
|
|
195
|
+
// intentionally not using regexes to extract functions and their content
|
|
196
|
+
// using cursor-based parsing gives better accuracy
|
|
197
|
+
while (currentPos < computedObj.length - 1) {
|
|
198
|
+
// -1 to exclude the closing brace
|
|
199
|
+
// Skip whitespace/comments so we don't match commented-out computed functions
|
|
200
|
+
currentPos = skipNonCode(computedObj, currentPos)
|
|
201
|
+
if (currentPos >= computedObj.length - 1) break
|
|
202
|
+
|
|
203
|
+
// Find the next function name (computed properties are `name() { ... }`)
|
|
204
|
+
const functionNameMatch = /^(\w+)\s*\(\)\s*/.exec(computedObj.substring(currentPos))
|
|
205
|
+
if (!functionNameMatch) {
|
|
206
|
+
const nextPos = skipComputedMember(computedObj, currentPos)
|
|
207
|
+
if (nextPos === -1) break
|
|
208
|
+
currentPos = nextPos
|
|
209
|
+
continue
|
|
180
210
|
}
|
|
181
211
|
|
|
182
|
-
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (thisRefs.size > 0) {
|
|
196
|
-
// Skip if already modified
|
|
197
|
-
if (
|
|
198
|
-
prop.body.includes(commentText.trim()) ||
|
|
199
|
-
Array.from(thisRefs).some((ref) => {
|
|
200
|
-
if (prop.body.startsWith(ref)) return true
|
|
201
|
-
// stripped && guard: body starts with the first segment, e.g. 'this.media &&'
|
|
202
|
-
const firstSegEnd = ref.indexOf('.', 5)
|
|
203
|
-
return firstSegEnd !== -1 && prop.body.startsWith(ref.substring(0, firstSegEnd))
|
|
204
|
-
})
|
|
205
|
-
) {
|
|
206
|
-
continue
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// reactivity code
|
|
210
|
-
const refCode = Array.from(thisRefs)
|
|
211
|
-
.map((ref) => `${toSafeRef(ref)};`)
|
|
212
|
-
.join(' ')
|
|
213
|
-
|
|
214
|
-
// replacement with the same whitespace
|
|
215
|
-
const openBraceIndex = prop.fullText.indexOf('{')
|
|
216
|
-
|
|
217
|
-
if (openBraceIndex !== -1) {
|
|
218
|
-
// Extract indentation from the original function body
|
|
219
|
-
const indentMatch = prop.body.match(/^(\s+)/)
|
|
220
|
-
const indent = indentMatch ? indentMatch[1] + ' ' : ' ' // Default to 4 spaces if no indent found
|
|
221
|
-
|
|
222
|
-
const modifiedPropText =
|
|
223
|
-
prop.fullText.substring(0, openBraceIndex + 1) +
|
|
224
|
-
'\n' +
|
|
225
|
-
indent +
|
|
226
|
-
commentText +
|
|
227
|
-
'\n' +
|
|
228
|
-
indent +
|
|
229
|
-
refCode +
|
|
230
|
-
'\n' +
|
|
231
|
-
indent +
|
|
232
|
-
prop.body +
|
|
233
|
-
'\n' +
|
|
234
|
-
prop.fullText.substring(0, openBraceIndex).match(/^(\s*)/)[0] +
|
|
235
|
-
'}'
|
|
236
|
-
|
|
237
|
-
replacements.push({
|
|
238
|
-
start: prop.startPos,
|
|
239
|
-
end: prop.endPos,
|
|
240
|
-
text: modifiedPropText,
|
|
241
|
-
})
|
|
242
|
-
}
|
|
243
|
-
}
|
|
212
|
+
const funcName = functionNameMatch[1]
|
|
213
|
+
const funcNamePos = currentPos
|
|
214
|
+
|
|
215
|
+
// Find opening brace for this function
|
|
216
|
+
const afterSignaturePos = funcNamePos + functionNameMatch[0].length
|
|
217
|
+
const openingBracePos = computedObj.indexOf('{', afterSignaturePos)
|
|
218
|
+
if (openingBracePos === -1) break
|
|
219
|
+
|
|
220
|
+
// Find closing brace for individual computed function
|
|
221
|
+
const closingBracePos = findComputedFunctionEnd(computedObj, openingBracePos)
|
|
222
|
+
|
|
223
|
+
if (closingBracePos === -1) {
|
|
224
|
+
break
|
|
244
225
|
}
|
|
245
226
|
|
|
246
|
-
|
|
247
|
-
|
|
227
|
+
// Extract the entire function and its body
|
|
228
|
+
// This is better and more accurate than using regex to avoid nested braces
|
|
229
|
+
const functionStartPos = funcNamePos
|
|
230
|
+
const functionEndPos = closingBracePos
|
|
231
|
+
const fullFunction = computedObj.substring(functionStartPos, functionEndPos)
|
|
232
|
+
const functionBody = computedObj.substring(openingBracePos + 1, closingBracePos - 1).trim()
|
|
233
|
+
|
|
234
|
+
computedProps.push({
|
|
235
|
+
name: funcName,
|
|
236
|
+
fullText: fullFunction,
|
|
237
|
+
body: functionBody,
|
|
238
|
+
startPos: functionStartPos,
|
|
239
|
+
endPos: functionEndPos,
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
// Move past this function
|
|
243
|
+
currentPos = closingBracePos
|
|
244
|
+
}
|
|
248
245
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
246
|
+
// Store the original computed object for later replacement
|
|
247
|
+
const originalComputedObj = configObject.substring(computedBlockStart, computedBlockEnd)
|
|
248
|
+
const computedObjOffsetInOriginal = computedStart - computedBlockStart
|
|
249
|
+
const computedObjEndInOriginal = computedObjOffsetInOriginal + computedObj.length
|
|
250
|
+
|
|
251
|
+
let modifiedComputedObj = originalComputedObj
|
|
252
|
+
let hasChanges = false
|
|
253
|
+
|
|
254
|
+
const replacements = []
|
|
255
|
+
|
|
256
|
+
for (const prop of computedProps) {
|
|
257
|
+
const thisRefs = extractThisReferences(prop.body)
|
|
258
|
+
|
|
259
|
+
if (thisRefs.size > 0) {
|
|
260
|
+
// Skip if already modified
|
|
261
|
+
if (
|
|
262
|
+
prop.body.includes(commentText.trim()) ||
|
|
263
|
+
Array.from(thisRefs).some((ref) => {
|
|
264
|
+
if (prop.body.startsWith(ref)) return true
|
|
265
|
+
// stripped && guard: body starts with the first segment, e.g. 'this.media &&'
|
|
266
|
+
const firstSegEnd = ref.indexOf('.', 5)
|
|
267
|
+
return firstSegEnd !== -1 && prop.body.startsWith(ref.substring(0, firstSegEnd))
|
|
268
|
+
})
|
|
269
|
+
) {
|
|
270
|
+
continue
|
|
253
271
|
}
|
|
254
272
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
273
|
+
// reactivity code
|
|
274
|
+
const refCode = Array.from(thisRefs)
|
|
275
|
+
.map((ref) => `${toSafeRef(ref)};`)
|
|
276
|
+
.join(' ')
|
|
277
|
+
|
|
278
|
+
// replacement with the same whitespace
|
|
279
|
+
const openBraceIndex = prop.fullText.indexOf('{')
|
|
280
|
+
|
|
281
|
+
if (openBraceIndex !== -1) {
|
|
282
|
+
// Extract indentation from the original function body
|
|
283
|
+
const indentMatch = prop.body.match(/^(\s+)/)
|
|
284
|
+
const indent = indentMatch ? indentMatch[1] + ' ' : ' ' // Default to 4 spaces if no indent found
|
|
285
|
+
|
|
286
|
+
const modifiedPropText =
|
|
287
|
+
prop.fullText.substring(0, openBraceIndex + 1) +
|
|
288
|
+
'\n' +
|
|
289
|
+
indent +
|
|
290
|
+
commentText +
|
|
291
|
+
'\n' +
|
|
292
|
+
indent +
|
|
293
|
+
refCode +
|
|
294
|
+
'\n' +
|
|
295
|
+
indent +
|
|
296
|
+
prop.body +
|
|
297
|
+
'\n' +
|
|
298
|
+
prop.fullText.substring(0, openBraceIndex).match(/^(\s*)/)[0] +
|
|
299
|
+
'}'
|
|
300
|
+
|
|
301
|
+
replacements.push({
|
|
302
|
+
start: prop.startPos,
|
|
303
|
+
end: prop.endPos,
|
|
304
|
+
text: modifiedPropText,
|
|
305
|
+
})
|
|
306
|
+
}
|
|
260
307
|
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (replacements.length > 0) {
|
|
311
|
+
let modifiedComputedInner = computedObj
|
|
261
312
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
replacement: modifiedComputedObj,
|
|
267
|
-
})
|
|
313
|
+
replacements.sort((a, b) => b.start - a.start)
|
|
314
|
+
for (const r of replacements) {
|
|
315
|
+
modifiedComputedInner =
|
|
316
|
+
modifiedComputedInner.slice(0, r.start) + r.text + modifiedComputedInner.slice(r.end)
|
|
268
317
|
}
|
|
318
|
+
|
|
319
|
+
modifiedComputedObj =
|
|
320
|
+
originalComputedObj.slice(0, computedObjOffsetInOriginal) +
|
|
321
|
+
modifiedComputedInner +
|
|
322
|
+
originalComputedObj.slice(computedObjEndInOriginal)
|
|
323
|
+
hasChanges = true
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (hasChanges) {
|
|
327
|
+
modifications.push({
|
|
328
|
+
original: originalComputedObj,
|
|
329
|
+
replacement: modifiedComputedObj,
|
|
330
|
+
})
|
|
269
331
|
}
|
|
270
332
|
}
|
|
271
333
|
}
|
|
272
334
|
|
|
335
|
+
return modifications
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export default (code) => {
|
|
339
|
+
const componentOpenRegex = /Blits\.(Component|Application)\s*\(/g
|
|
340
|
+
const modifications = []
|
|
341
|
+
|
|
342
|
+
let match
|
|
343
|
+
while ((match = componentOpenRegex.exec(code)) !== null) {
|
|
344
|
+
const openParenPos = match.index + match[0].length - 1
|
|
345
|
+
const configStart = findConfigBraceStart(code, openParenPos)
|
|
346
|
+
if (configStart === -1) continue
|
|
347
|
+
|
|
348
|
+
const configEnd = findMatchingBrace(code, configStart)
|
|
349
|
+
if (configEnd === -1) continue
|
|
350
|
+
|
|
351
|
+
const configObject = code.substring(configStart, configEnd + 1)
|
|
352
|
+
modifications.push(...processComputedBlock(configObject))
|
|
353
|
+
}
|
|
354
|
+
|
|
273
355
|
// Apply all modifications at once, from last to first to preserve positions
|
|
274
356
|
if (modifications.length > 0) {
|
|
275
357
|
let modifiedCode = code
|
package/src/router/router.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* SPDX-License-Identifier: Apache-2.0
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { getHash, isObject, isString, matchHash } from './utils.js'
|
|
18
|
+
import { getHash, isObject, isString, matchHash, sameRouteObject, setHash } from './utils.js'
|
|
19
19
|
import { reactive } from '../lib/reactivity/reactive.js'
|
|
20
20
|
|
|
21
21
|
import symbols from '../lib/symbols.js'
|
|
@@ -52,7 +52,8 @@ import Settings from '../settings.js'
|
|
|
52
52
|
*/
|
|
53
53
|
|
|
54
54
|
/** @type {Route} */
|
|
55
|
-
export let currentRoute
|
|
55
|
+
export let currentRoute = {}
|
|
56
|
+
|
|
56
57
|
export const state = reactive(
|
|
57
58
|
{
|
|
58
59
|
path: '',
|
|
@@ -76,6 +77,8 @@ export const state = reactive(
|
|
|
76
77
|
*/
|
|
77
78
|
|
|
78
79
|
const history = []
|
|
80
|
+
const routerViews = new Set()
|
|
81
|
+
let singleRouterView = null
|
|
79
82
|
|
|
80
83
|
let overrideOptions = {}
|
|
81
84
|
let navigationData = {}
|
|
@@ -105,12 +108,14 @@ export const navigate = async function () {
|
|
|
105
108
|
if (!this[symbols.parent][symbols.routes] || this[symbols.parent][symbols.routes].length === 0)
|
|
106
109
|
return
|
|
107
110
|
|
|
111
|
+
if (this.history === undefined) this.history = []
|
|
112
|
+
|
|
108
113
|
state.navigating = true
|
|
109
114
|
|
|
110
115
|
Announcer.stop()
|
|
111
116
|
Announcer.clear()
|
|
112
117
|
|
|
113
|
-
const hash = getHash(location.hash)
|
|
118
|
+
const hash = getHash(location.hash, this.name)
|
|
114
119
|
// try to find the route
|
|
115
120
|
let route = matchHash(hash, this[symbols.parent][symbols.routes], overrideOptions, navigationData)
|
|
116
121
|
|
|
@@ -126,36 +131,46 @@ export const navigate = async function () {
|
|
|
126
131
|
return
|
|
127
132
|
}
|
|
128
133
|
|
|
134
|
+
// sameRouteObject is too simple check for now!
|
|
135
|
+
if (this.currentRoute !== undefined && sameRouteObject(route, this.currentRoute)) {
|
|
136
|
+
state.navigating = false
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
129
140
|
let reuse = false
|
|
130
|
-
|
|
131
|
-
currentRoute = route
|
|
132
|
-
const currentPath = currentRoute.path
|
|
141
|
+
this.previousRoute = this.currentRoute
|
|
142
|
+
this.currentRoute = route
|
|
143
|
+
const currentPath = this.currentRoute.path
|
|
133
144
|
|
|
134
145
|
// execute before each hook
|
|
135
|
-
const beforeEachResult = await executeBeforeHook(
|
|
146
|
+
const beforeEachResult = await executeBeforeHook.call(
|
|
147
|
+
this,
|
|
136
148
|
this[symbols.parent][symbols.routerHooks],
|
|
137
149
|
'beforeEach',
|
|
138
150
|
this[symbols.parent],
|
|
139
151
|
route,
|
|
140
|
-
previousRoute,
|
|
152
|
+
this.previousRoute,
|
|
141
153
|
currentPath
|
|
142
154
|
)
|
|
143
155
|
if (beforeEachResult === false) {
|
|
144
156
|
preventHashChangeNavigation = false
|
|
157
|
+
state.navigating = false
|
|
145
158
|
return
|
|
146
159
|
}
|
|
147
160
|
|
|
148
161
|
// execute before route hook
|
|
149
|
-
const beforeResult = await executeBeforeHook(
|
|
162
|
+
const beforeResult = await executeBeforeHook.call(
|
|
163
|
+
this,
|
|
150
164
|
route.hooks,
|
|
151
165
|
'before',
|
|
152
166
|
this[symbols.parent],
|
|
153
167
|
route,
|
|
154
|
-
previousRoute,
|
|
168
|
+
this.previousRoute,
|
|
155
169
|
currentPath
|
|
156
170
|
)
|
|
157
171
|
if (beforeResult === false) {
|
|
158
172
|
preventHashChangeNavigation = false
|
|
173
|
+
state.navigating = false
|
|
159
174
|
return
|
|
160
175
|
}
|
|
161
176
|
|
|
@@ -165,18 +180,18 @@ export const navigate = async function () {
|
|
|
165
180
|
// FIX: use truthy check instead of `!== undefined` because matchHash()
|
|
166
181
|
// can return `false`, which survives `!== undefined` but has no `.options`.
|
|
167
182
|
if (
|
|
168
|
-
previousRoute &&
|
|
169
|
-
previousRoute.options &&
|
|
170
|
-
previousRoute.options.inHistory === true &&
|
|
183
|
+
this.previousRoute &&
|
|
184
|
+
this.previousRoute.options &&
|
|
185
|
+
this.previousRoute.options.inHistory === true &&
|
|
171
186
|
navigatingBack === false
|
|
172
187
|
) {
|
|
173
|
-
history.push(previousRoute)
|
|
188
|
+
this.history.push(this.previousRoute)
|
|
174
189
|
}
|
|
175
190
|
|
|
176
191
|
// a transition can be a function returning a dynamic transition object
|
|
177
192
|
// based on current and previous route
|
|
178
193
|
if (typeof route.transition === 'function') {
|
|
179
|
-
route.transition = route.transition(previousRoute, route)
|
|
194
|
+
route.transition = route.transition(this.previousRoute, route)
|
|
180
195
|
}
|
|
181
196
|
|
|
182
197
|
/** @type {import('../engines/L3/element.js').BlitsElement} */
|
|
@@ -201,10 +216,10 @@ export const navigate = async function () {
|
|
|
201
216
|
// see if the component of the previous route can be reused for the
|
|
202
217
|
// current route
|
|
203
218
|
if (
|
|
204
|
-
previousRoute &&
|
|
219
|
+
this.previousRoute &&
|
|
205
220
|
route.options.reuseComponent === true &&
|
|
206
221
|
route.options.keepAlive !== true &&
|
|
207
|
-
route.component === previousRoute.component
|
|
222
|
+
route.component === this.previousRoute.component
|
|
208
223
|
) {
|
|
209
224
|
reuse = true
|
|
210
225
|
view = this[symbols.children][this[symbols.children].length - 1]
|
|
@@ -288,28 +303,30 @@ export const navigate = async function () {
|
|
|
288
303
|
// apply out out transition on previous view if available, unless
|
|
289
304
|
// we're reusing the prvious page component
|
|
290
305
|
// FIX: truthy guard — previousRoute can be `false` (see history-push comment above).
|
|
291
|
-
if (previousRoute && reuse === false) {
|
|
306
|
+
if (this.previousRoute && reuse === false) {
|
|
292
307
|
// only animate when there is a previous route
|
|
293
308
|
shouldAnimate = true
|
|
294
309
|
let oldView = this[symbols.children].splice(1, 1).pop()
|
|
295
310
|
if (oldView) {
|
|
296
|
-
executeTransition(previousRoute.transition.out, oldView[symbols.holder], true)
|
|
311
|
+
executeTransition(this.previousRoute.transition.out, oldView[symbols.holder], true)
|
|
297
312
|
|
|
298
313
|
// Resolve effective keepAlive: runtime override from $router.to() takes precedence
|
|
299
314
|
// over the static route config option
|
|
315
|
+
const hasOverrideOptions = overrideOptions && typeof overrideOptions === 'object'
|
|
300
316
|
const keepAlive =
|
|
301
|
-
overrideOptions.keepAlive !== undefined
|
|
317
|
+
hasOverrideOptions && overrideOptions.keepAlive !== undefined
|
|
302
318
|
? overrideOptions.keepAlive
|
|
303
|
-
: previousRoute.options && previousRoute.options.keepAlive
|
|
319
|
+
: this.previousRoute.options && this.previousRoute.options.keepAlive
|
|
304
320
|
|
|
305
321
|
// cache the page when it's as 'keepAlive' instead of destroying
|
|
306
322
|
if (
|
|
307
323
|
navigatingBack === false &&
|
|
308
324
|
keepAlive === true &&
|
|
309
|
-
previousRoute
|
|
310
|
-
previousRoute.options
|
|
325
|
+
this.previousRoute &&
|
|
326
|
+
this.previousRoute.options &&
|
|
327
|
+
this.previousRoute.options.inHistory === true
|
|
311
328
|
) {
|
|
312
|
-
const historyItem = history[history.length - 1]
|
|
329
|
+
const historyItem = this.history[this.history.length - 1]
|
|
313
330
|
if (historyItem !== undefined) {
|
|
314
331
|
historyItem.view = oldView
|
|
315
332
|
historyItem.focus = previousFocus
|
|
@@ -321,7 +338,7 @@ export const navigate = async function () {
|
|
|
321
338
|
* 2. Navigating back, and the previous route is configured with "keep alive" set to true.
|
|
322
339
|
* 3. Navigating back, and the previous route is not configured with "keep alive" set to true.
|
|
323
340
|
*/
|
|
324
|
-
if (previousRoute.options && (keepAlive !== true || navigatingBack === true)) {
|
|
341
|
+
if (this.previousRoute.options && (keepAlive !== true || navigatingBack === true)) {
|
|
325
342
|
oldView.destroy()
|
|
326
343
|
oldView = null
|
|
327
344
|
}
|
|
@@ -339,11 +356,11 @@ export const navigate = async function () {
|
|
|
339
356
|
'afterEach',
|
|
340
357
|
this[symbols.parent],
|
|
341
358
|
route,
|
|
342
|
-
previousRoute
|
|
359
|
+
this.previousRoute
|
|
343
360
|
)
|
|
344
361
|
|
|
345
362
|
// execute after route Hook
|
|
346
|
-
await executeAfterHook(route.hooks, 'after', this[symbols.parent], route, previousRoute)
|
|
363
|
+
await executeAfterHook(route.hooks, 'after', this[symbols.parent], route, this.previousRoute)
|
|
347
364
|
|
|
348
365
|
// Clear module-level variables after removeView has consumed them.
|
|
349
366
|
// Placed here so it executes for all navigation flows, not only when
|
|
@@ -391,34 +408,36 @@ const executeBeforeHook = async function (
|
|
|
391
408
|
try {
|
|
392
409
|
result = await hooks[hookName].call(parent, route, previousRoute)
|
|
393
410
|
if (isString(result)) {
|
|
394
|
-
currentRoute = previousRoute
|
|
395
|
-
to(result)
|
|
411
|
+
this.currentRoute = previousRoute
|
|
412
|
+
to(result, {}, {}, this.name)
|
|
396
413
|
return false
|
|
397
414
|
}
|
|
398
415
|
} catch (error) {
|
|
399
416
|
Log.error(`Error or Rejected Promise in "${hookName}" Hook`, error)
|
|
400
|
-
|
|
417
|
+
this.currentRoute = previousRoute
|
|
418
|
+
if (this.history.length > 0) {
|
|
401
419
|
preventHashChangeNavigation = true
|
|
402
|
-
currentRoute = previousRoute
|
|
403
420
|
window.history.back()
|
|
404
421
|
navigatingBack = false
|
|
405
422
|
state.navigating = false
|
|
406
|
-
return false
|
|
407
423
|
}
|
|
424
|
+
return false
|
|
408
425
|
}
|
|
409
426
|
// If the resolved result is an object, redirect if the path in the object was changed
|
|
410
427
|
if (isObject(result) === true && result.path !== currentPath) {
|
|
411
|
-
currentRoute = previousRoute
|
|
412
|
-
to(result.path, result.data, result.options)
|
|
428
|
+
this.currentRoute = previousRoute
|
|
429
|
+
to(result.path, result.data, result.options, this.name)
|
|
413
430
|
return false
|
|
414
431
|
}
|
|
415
432
|
// If the resolved result is false, cancel navigation
|
|
416
|
-
if (result === false
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
433
|
+
if (result === false) {
|
|
434
|
+
this.currentRoute = previousRoute
|
|
435
|
+
if (this.history.length > 0) {
|
|
436
|
+
preventHashChangeNavigation = true
|
|
437
|
+
window.history.back()
|
|
438
|
+
navigatingBack = false
|
|
439
|
+
state.navigating = false
|
|
440
|
+
}
|
|
422
441
|
return false
|
|
423
442
|
}
|
|
424
443
|
}
|
|
@@ -471,24 +490,55 @@ const executeTransition = async (transition, element, animate) => {
|
|
|
471
490
|
}
|
|
472
491
|
}
|
|
473
492
|
|
|
474
|
-
export const to = (path, data = {}, options = {}) => {
|
|
493
|
+
export const to = (path, data = {}, options = {}, routerViewName = '') => {
|
|
475
494
|
navigationData = data
|
|
476
495
|
overrideOptions = options
|
|
477
496
|
|
|
478
|
-
|
|
497
|
+
setHash(path, routerViewName)
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export const toRouterView = (routerView, path, data = {}, options = {}) => {
|
|
501
|
+
navigationData = data
|
|
502
|
+
overrideOptions = options
|
|
503
|
+
|
|
504
|
+
setHash(path, routerView.name)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export const registerRouterView = (routerView) => {
|
|
508
|
+
routerViews.add(routerView)
|
|
509
|
+
singleRouterView = routerViews.size === 1 ? routerView : null
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
export const unregisterRouterView = (routerView) => {
|
|
513
|
+
routerViews.delete(routerView)
|
|
514
|
+
singleRouterView = routerViews.size === 1 ? routerViews.values().next().value : null
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export const getRegisteredRouterView = (name) => {
|
|
518
|
+
for (const routerView of routerViews) {
|
|
519
|
+
if (routerView.name === name) return routerView
|
|
520
|
+
}
|
|
521
|
+
return null
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
export const getRegisteredRouterViewsCount = () => routerViews.size
|
|
525
|
+
|
|
526
|
+
export const getSingleRegisteredRouterView = () => {
|
|
527
|
+
return singleRouterView
|
|
479
528
|
}
|
|
480
529
|
|
|
481
530
|
export const back = function () {
|
|
482
|
-
|
|
483
|
-
|
|
531
|
+
if (this.history === undefined) return
|
|
532
|
+
const route = this.history.pop()
|
|
533
|
+
if (route && this.currentRoute !== route) {
|
|
484
534
|
// set indicator that we are navigating back (to prevent adding page to history stack)
|
|
485
535
|
navigatingBack = true
|
|
486
536
|
navigatingBackTo = route
|
|
487
|
-
|
|
537
|
+
toRouterView(this, route.hash, route.data, route.options)
|
|
488
538
|
return true
|
|
489
539
|
}
|
|
490
540
|
|
|
491
|
-
const backtrack = (currentRoute && currentRoute.options.backtrack) || false
|
|
541
|
+
const backtrack = (this.currentRoute && this.currentRoute.options.backtrack) || false
|
|
492
542
|
|
|
493
543
|
// If we deeplink to a page without backtrack
|
|
494
544
|
// we we let the RouterView handle back
|
|
@@ -497,7 +547,7 @@ export const back = function () {
|
|
|
497
547
|
}
|
|
498
548
|
|
|
499
549
|
const hashEnd = /(\/:?[\w%\s-]+)$/
|
|
500
|
-
let path = currentRoute.path
|
|
550
|
+
let path = this.currentRoute.path
|
|
501
551
|
|
|
502
552
|
let level = path.split('/').length
|
|
503
553
|
|
|
@@ -520,7 +570,7 @@ export const back = function () {
|
|
|
520
570
|
)
|
|
521
571
|
|
|
522
572
|
if (route && backtrack) {
|
|
523
|
-
|
|
573
|
+
toRouterView(this, route.path, route.data, route.options)
|
|
524
574
|
return true
|
|
525
575
|
}
|
|
526
576
|
}
|
package/src/router/utils.js
CHANGED
|
@@ -4,16 +4,85 @@ import { default as fadeInFadeOutTransition } from './transitions/fadeInOut.js'
|
|
|
4
4
|
* Get the current hash
|
|
5
5
|
* @returns {Hash}
|
|
6
6
|
*/
|
|
7
|
-
export const getHash = (hash) => {
|
|
7
|
+
export const getHash = (hash, routerViewName = '') => {
|
|
8
8
|
if (!hash) hash = '/'
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
// split router views
|
|
11
|
+
const [main, ...named] = hash.split('|')
|
|
12
|
+
|
|
13
|
+
if (routerViewName !== '') {
|
|
14
|
+
const match = named.find((name) => name.startsWith(routerViewName + '='))
|
|
15
|
+
if (match !== undefined) {
|
|
16
|
+
hash = match.slice(routerViewName.length + 1)
|
|
17
|
+
} else {
|
|
18
|
+
hash = '/'
|
|
19
|
+
}
|
|
20
|
+
} else {
|
|
21
|
+
hash = main
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const [path = '', query = ''] = hash.replace(/^#/, '').split('?')
|
|
10
25
|
return {
|
|
11
|
-
path:
|
|
12
|
-
queryParams: new URLSearchParams(
|
|
26
|
+
path: path,
|
|
27
|
+
queryParams: new URLSearchParams(query),
|
|
13
28
|
hash: hash,
|
|
14
29
|
}
|
|
15
30
|
}
|
|
16
31
|
|
|
32
|
+
export const setHash = (path, routerViewName = '') => {
|
|
33
|
+
const cleanPath = (path || '').replace(/^#/, '')
|
|
34
|
+
const raw = (location.hash || '').replace(/^#/, '')
|
|
35
|
+
|
|
36
|
+
const parts = raw ? raw.split('|').filter(Boolean) : []
|
|
37
|
+
|
|
38
|
+
let newMain = '/'
|
|
39
|
+
const newNamed = {}
|
|
40
|
+
|
|
41
|
+
// Parse existing hash. The first segment is always the main route; only
|
|
42
|
+
// subsequent pipe-separated segments can be named router views.
|
|
43
|
+
for (let i = 0; i < parts.length; i++) {
|
|
44
|
+
const part = parts[i]
|
|
45
|
+
if (i === 0) {
|
|
46
|
+
newMain = part
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const eqIndex = part.indexOf('=')
|
|
51
|
+
|
|
52
|
+
// named route: sidebar=/search
|
|
53
|
+
if (eqIndex > 0) {
|
|
54
|
+
const name = part.slice(0, eqIndex)
|
|
55
|
+
const value = part.slice(eqIndex + 1)
|
|
56
|
+
if (value) newNamed[name] = value
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// unnamed/main route
|
|
61
|
+
if (part) {
|
|
62
|
+
newMain = part
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Update target router view
|
|
67
|
+
if (routerViewName === '') {
|
|
68
|
+
newMain = cleanPath || '/'
|
|
69
|
+
} else {
|
|
70
|
+
if (cleanPath) {
|
|
71
|
+
newNamed[routerViewName] = cleanPath
|
|
72
|
+
} else {
|
|
73
|
+
delete newNamed[routerViewName]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Rebuild hash: main always first
|
|
78
|
+
let newHash = newMain || '/'
|
|
79
|
+
|
|
80
|
+
for (const [name, value] of Object.entries(newNamed)) {
|
|
81
|
+
newHash += `|${name}=${value}`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
location.hash = newHash
|
|
85
|
+
}
|
|
17
86
|
export const normalizePath = (path) => {
|
|
18
87
|
return (
|
|
19
88
|
path
|
|
@@ -60,17 +129,25 @@ const defaultOptions = {
|
|
|
60
129
|
}
|
|
61
130
|
|
|
62
131
|
export const makeRouteObject = (route, overrides, overrideOptions, navigationData) => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
//
|
|
66
|
-
|
|
132
|
+
const options = { ...defaultOptions, ...route.options }
|
|
133
|
+
|
|
134
|
+
// keepAlive applies to the route being left, so do not merge it into the destination.
|
|
135
|
+
if (isObject(overrideOptions)) {
|
|
136
|
+
const keys = Object.keys(overrideOptions)
|
|
137
|
+
for (let i = 0; i < keys.length; i++) {
|
|
138
|
+
const key = keys[i]
|
|
139
|
+
if (key !== 'keepAlive') {
|
|
140
|
+
options[key] = overrideOptions[key]
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
67
144
|
|
|
68
145
|
const cleanRoute = {
|
|
69
146
|
hash: overrides.hash,
|
|
70
147
|
path: route.path,
|
|
71
148
|
component: route.component,
|
|
72
149
|
transition: 'transition' in route ? route.transition : fadeInFadeOutTransition,
|
|
73
|
-
options
|
|
150
|
+
options,
|
|
74
151
|
announce: route.announce || false,
|
|
75
152
|
hooks: route.hooks || {},
|
|
76
153
|
data: { ...route.data, ...navigationData, ...overrides.queryParams },
|
|
@@ -156,3 +233,13 @@ export const matchHash = (
|
|
|
156
233
|
// @ts-ignore - Remove me when we have a better way to handle this
|
|
157
234
|
return matchingRoute
|
|
158
235
|
}
|
|
236
|
+
|
|
237
|
+
export const sameRouteObject = (route1, route2) => {
|
|
238
|
+
if (route1 === null) return false
|
|
239
|
+
if (route2 === null) return false
|
|
240
|
+
|
|
241
|
+
// this is too simple a check for now
|
|
242
|
+
if (route1.hash !== route2.hash) return false
|
|
243
|
+
|
|
244
|
+
return true
|
|
245
|
+
}
|