1 | import getApp from './app/get'
|
2 | import getContextAndFocus from './router/get-context-and-focus'
|
3 | import getIndexOfPanelToShow from './runtime/get-index-of-panel-to-show'
|
4 | import getNextPosition from './runtime/get-next-position'
|
5 | import getRegions from './runtime/get-regions'
|
6 | import normaliseUri from './utils/normalise-uri/index.js'
|
7 | import parse from './router/parse'
|
8 |
|
9 | function ensurePanelShape(panel) {
|
10 | if (typeof panel.width === 'undefined') {
|
11 | panel.width = 360
|
12 | }
|
13 | }
|
14 |
|
15 | export const NAVIGATE = 'panels/NAVIGATE'
|
16 | export function navigate(rawUri, nextFocus = 1, nextContext) {
|
17 | return async function navigateThunk(dispatch, getState) {
|
18 | const { apps, panels, router, runtime } = getState()
|
19 |
|
20 | const uri = normaliseUri(rawUri)
|
21 | if (uri === router.uri) {
|
22 | return
|
23 | }
|
24 | const parsed = parse(uri, router.parsers)
|
25 |
|
26 | const routes = {
|
27 | byContext: parsed.routes.byContext,
|
28 | items: parsed.routes.items,
|
29 | }
|
30 |
|
31 | const createAppContext = (appName, appModule) => {
|
32 | const access = async name => {
|
33 | const app = getState().apps.byName[name]
|
34 |
|
35 | if (app) {
|
36 | if (app.access(appName, appModule)) {
|
37 | return app.store
|
38 | } else {
|
39 | throw Error(`Access to ${name} denied.`)
|
40 | }
|
41 | } else {
|
42 | throw Error(`App ${name} doesn't exist or it doesn't have a store.`)
|
43 | }
|
44 | }
|
45 |
|
46 | return {
|
47 | access,
|
48 | navigate: (...args) => dispatch(navigate(...args)),
|
49 | routes,
|
50 | }
|
51 | }
|
52 |
|
53 |
|
54 | const nextApps = {
|
55 | byName: {},
|
56 | items: parsed.apps.items.filter(name => apps.items.indexOf(name) === -1),
|
57 | }
|
58 |
|
59 | if (nextApps.items.length) {
|
60 | dispatch({
|
61 | type: NAVIGATE,
|
62 | sequence: {
|
63 | type: 'start',
|
64 | },
|
65 | meta: {
|
66 | uri,
|
67 | },
|
68 | })
|
69 | }
|
70 |
|
71 | await Promise.all(
|
72 | nextApps.items.map(async name => {
|
73 | try {
|
74 |
|
75 | nextApps.byName[name] = await getApp(name, createAppContext)
|
76 | } catch (error) {
|
77 |
|
78 | console.error(`Can't load app ${name}`, error)
|
79 | }
|
80 | })
|
81 | )
|
82 |
|
83 | const nextPanels = {
|
84 | byId: {},
|
85 | items: [],
|
86 | }
|
87 |
|
88 |
|
89 | parsed.apps.items.forEach(name => {
|
90 |
|
91 | const panelsToLoad = parsed.apps.byName[name].panels
|
92 |
|
93 | const app = nextApps.byName[name] || apps.byName[name]
|
94 |
|
95 | panelsToLoad.forEach(path => {
|
96 | const panelId = `${name}${path}`
|
97 |
|
98 | if (panels.byId[panelId]) {
|
99 | return
|
100 | }
|
101 |
|
102 | try {
|
103 |
|
104 | let { panel, props } = app.findPanel(path)
|
105 |
|
106 | if (typeof panel === 'function') {
|
107 | panel = panel(props, app.store)
|
108 | } else {
|
109 | panel = {
|
110 | ...panel,
|
111 | props: props,
|
112 | }
|
113 | }
|
114 |
|
115 |
|
116 | ensurePanelShape(panel)
|
117 |
|
118 | nextPanels.byId[panelId] = panel
|
119 | nextPanels.items.push(panelId)
|
120 | } catch (error) {
|
121 |
|
122 | console.error(`Can't load panel ${panelId}`, error)
|
123 | }
|
124 | })
|
125 | })
|
126 |
|
127 | const maxFullPanelWidth = runtime.viewportWidth - runtime.snapPoint
|
128 | const isFirstLoad = typeof router.focus === 'undefined'
|
129 | const last = parsed.routes.items.length - 1
|
130 |
|
131 | const opts = {
|
132 | currentFocus: router.focus,
|
133 | next: {
|
134 | context: nextContext,
|
135 | focus: nextFocus,
|
136 | },
|
137 | uri,
|
138 | last,
|
139 | }
|
140 | if (isFirstLoad) {
|
141 | const focusRoute = parsed.routes.byContext[parsed.routes.items[last]]
|
142 | const focusPanel = nextPanels.byId[focusRoute.panelId]
|
143 | opts.currentFocus = last
|
144 | opts.next.focus = 0
|
145 | if (typeof focusPanel.context !== 'undefined') {
|
146 | opts.next.context = focusPanel.context
|
147 | }
|
148 | }
|
149 | const { context, focus } = getContextAndFocus(opts)
|
150 |
|
151 | const widths = routes.items.map(routeContext => {
|
152 |
|
153 |
|
154 |
|
155 |
|
156 | const route = routes.byContext[routeContext]
|
157 | const panel = panels.byId[route.panelId] || nextPanels.byId[route.panelId]
|
158 |
|
159 | let width
|
160 | if (route.isVisible) {
|
161 | if (runtime.shouldGoMobile) {
|
162 | width = runtime.viewportWidth
|
163 | } else {
|
164 | const prevRoute = router.routes.byContext[routeContext]
|
165 | width = route.isExpanded
|
166 | ? panel.maxWidth
|
167 | : (prevRoute && prevRoute.width) || panel.width
|
168 |
|
169 | const percentageMatch =
|
170 | typeof width === 'string' && width.match(/([0-9]+)%/)
|
171 | if (percentageMatch) {
|
172 | width = maxFullPanelWidth * parseInt(percentageMatch, 10) / 100
|
173 | }
|
174 | }
|
175 | } else {
|
176 | width = 0
|
177 | }
|
178 |
|
179 | route.width = width
|
180 | return width
|
181 | })
|
182 |
|
183 |
|
184 | const focusWidth = widths[focus]
|
185 |
|
186 | let x = widths.slice(0, focus).reduce((a, b) => a + b, 0)
|
187 |
|
188 | let leftForContext = maxFullPanelWidth - focusWidth
|
189 |
|
190 | let contextsLeft = focus - context
|
191 |
|
192 |
|
193 | while (contextsLeft > 0) {
|
194 |
|
195 | contextsLeft--
|
196 |
|
197 |
|
198 | const contextWidth = widths[contextsLeft]
|
199 |
|
200 |
|
201 | if (leftForContext < contextWidth) {
|
202 | break
|
203 | }
|
204 |
|
205 |
|
206 | leftForContext -= contextWidth
|
207 |
|
208 | x -= contextWidth
|
209 | }
|
210 |
|
211 | const regions = getRegions(widths)
|
212 | const snappedAt = getIndexOfPanelToShow(x, regions)
|
213 |
|
214 | dispatch({
|
215 | type: NAVIGATE,
|
216 | sequence: {
|
217 | type: 'next',
|
218 | },
|
219 | payload: {
|
220 | apps: nextApps,
|
221 | panels: nextPanels,
|
222 | router: {
|
223 | context,
|
224 | focus,
|
225 | routes,
|
226 | uri,
|
227 | },
|
228 | runtime: {
|
229 | maxFullPanelWidth,
|
230 | regions,
|
231 | snappedAt,
|
232 | width: maxFullPanelWidth + widths.reduce((a, b) => a + b, 0),
|
233 | widths,
|
234 | x,
|
235 | },
|
236 | },
|
237 | meta: {
|
238 | uri,
|
239 | },
|
240 | })
|
241 | }
|
242 | }
|
243 |
|
244 | export const TOGGLE_EXPAND = 'panels/panels/TOGGLE_EXPAND'
|
245 | export function toggleExpand(routeContext) {
|
246 | return function toggleExpandThunk(dispatch, getState) {
|
247 | const { panels, router, runtime } = getState()
|
248 |
|
249 | const routes = router.routes
|
250 | const route = routes.byContext[routeContext]
|
251 |
|
252 | routes.byContext = {
|
253 | ...routes.byContext,
|
254 | [routeContext]: {
|
255 | ...route,
|
256 | isExpanded: !route.isExpanded,
|
257 | },
|
258 | }
|
259 |
|
260 | const nextPosition = getNextPosition({
|
261 |
|
262 | context: router.context,
|
263 | focus: router.focus,
|
264 | maxFullPanelWidth: runtime.maxFullPanelWidth,
|
265 | routes,
|
266 | panels,
|
267 | shouldGoMobile: runtime.shouldGoMobile,
|
268 | viewportWidth: runtime.viewportWidth,
|
269 | })
|
270 |
|
271 | dispatch({
|
272 | type: TOGGLE_EXPAND,
|
273 | payload: nextPosition,
|
274 | })
|
275 | }
|
276 | }
|
277 |
|
278 | export const UPDATE_SETTINGS = 'panels/panels/UPDATE_SETTINGS'
|
279 | export function updateSettings(
|
280 | routeContext,
|
281 | { maxWidth, title, styleBackground, width }
|
282 | ) {
|
283 | return function updateSettingsThunk(dispatch, getState) {
|
284 | const { panels, router, runtime } = getState()
|
285 |
|
286 | const nextPanels = {
|
287 | byId: panels.byId,
|
288 | items: panels.items,
|
289 | }
|
290 |
|
291 | const route = router.routes.byContext[routeContext]
|
292 |
|
293 | if (!route.isVisible) return
|
294 |
|
295 | const panel = {
|
296 | ...nextPanels.byId[route.panelId],
|
297 | }
|
298 |
|
299 | if (maxWidth) {
|
300 | panel.maxWidth = maxWidth
|
301 | }
|
302 | if (title) {
|
303 | panel.title = title
|
304 | }
|
305 | if (styleBackground) {
|
306 | panel.styleBackground = styleBackground
|
307 | }
|
308 | if (width) {
|
309 | panel.width = width
|
310 | }
|
311 |
|
312 | nextPanels.byId = {
|
313 | ...nextPanels.byId,
|
314 | [route.panelId]: panel,
|
315 | }
|
316 |
|
317 | let nextPosition
|
318 | if (maxWidth || width) {
|
319 | nextPosition = getNextPosition({
|
320 | context: router.context,
|
321 | focus: router.focus,
|
322 | maxFullPanelWidth: runtime.maxFullPanelWidth,
|
323 | routes: router.routes,
|
324 | panels: nextPanels,
|
325 | shouldGoMobile: runtime.shouldGoMobile,
|
326 | viewportWidth: runtime.viewportWidth,
|
327 | })
|
328 | }
|
329 |
|
330 | dispatch({
|
331 | type: UPDATE_SETTINGS,
|
332 | payload: {
|
333 | nextPanelsById: nextPanels.byId,
|
334 | nextPosition,
|
335 | },
|
336 | })
|
337 | }
|
338 | }
|