UNPKG

8.94 kBJavaScriptView Raw
1import getApp from './app/get'
2import getContextAndFocus from './router/get-context-and-focus'
3import getIndexOfPanelToShow from './runtime/get-index-of-panel-to-show'
4import getNextPosition from './runtime/get-next-position'
5import getRegions from './runtime/get-regions'
6import normaliseUri from './utils/normalise-uri/index.js'
7import parse from './router/parse'
8
9function ensurePanelShape(panel) {
10 if (typeof panel.width === 'undefined') {
11 panel.width = 360
12 }
13}
14
15export const NAVIGATE = 'panels/NAVIGATE'
16export 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 // get the next apps
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 // otherwise fetch it! :)
75 nextApps.byName[name] = await getApp(name, createAppContext)
76 } catch (error) {
77 // TODO
78 console.error(`Can't load app ${name}`, error)
79 }
80 })
81 )
82
83 const nextPanels = {
84 byId: {},
85 items: [],
86 }
87
88 // we still need to go through all the apps
89 parsed.apps.items.forEach(name => {
90 // get the list of panels to load
91 const panelsToLoad = parsed.apps.byName[name].panels
92 // get the app
93 const app = nextApps.byName[name] || apps.byName[name]
94
95 panelsToLoad.forEach(path => {
96 const panelId = `${name}${path}`
97 // exit early if the panel is already loaded
98 if (panels.byId[panelId]) {
99 return
100 }
101
102 try {
103 // find the panel within the app
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 // ensure that the panel has a valid shape and defaults
116 ensurePanelShape(panel)
117
118 nextPanels.byId[panelId] = panel
119 nextPanels.items.push(panelId)
120 } catch (error) {
121 // TODO
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 // determine the context and focus panel
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 // if (routes.byContext[routeContext]) {
153 // return routes.byContext[routeContext].width
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 // get how large our focus panel is
184 const focusWidth = widths[focus] // >> 500
185 // get the focus panel's x real position in our runtime if it were flat
186 let x = widths.slice(0, focus).reduce((a, b) => a + b, 0) // >> 860
187 // get how much space we have left for context panels
188 let leftForContext = maxFullPanelWidth - focusWidth // >> 1089
189 // assess how many context panels we should try to show
190 let contextsLeft = focus - context
191
192 // try to fit those context panels within that space that's left
193 while (contextsLeft > 0) {
194 // decrease the amount of contexts left
195 contextsLeft--
196
197 // get the context's width
198 const contextWidth = widths[contextsLeft]
199
200 // check if we have space left for this panel to be a visible context panel
201 if (leftForContext < contextWidth) {
202 break
203 }
204
205 // if we do, remove it from the space left for context
206 leftForContext -= contextWidth
207 // shift x to include that panel
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
244export const TOGGLE_EXPAND = 'panels/panels/TOGGLE_EXPAND'
245export 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 // snap at the expanded position!
262 context: router.context, // routeIndex - runtime.snappedAt,
263 focus: router.focus, // routeIndex,
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
278export const UPDATE_SETTINGS = 'panels/panels/UPDATE_SETTINGS'
279export 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}