@trullock/page-manager 1.2.1 → 1.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trullock/page-manager",
3
- "version": "1.2.1",
3
+ "version": "1.3.1",
4
4
  "description": "Single page app manager",
5
5
  "exports": {
6
6
  ".": "./src/index.js"
@@ -11,7 +11,7 @@
11
11
  "build": "webpack --mode=development",
12
12
  "tests": "node ./tests/tests.js"
13
13
  },
14
- "author": "",
14
+ "author": "Trullock",
15
15
  "license": "ISC",
16
16
  "publishConfig": {
17
17
  "access": "public"
package/src/index.js CHANGED
@@ -7,7 +7,7 @@ var pageCache = {},
7
7
 
8
8
  var manuallyAdjustingHistory = false;
9
9
  var handlingBeforeHide = false;
10
- var lastNavigationDirection = null;
10
+ var beforeHideNavDirection = null;
11
11
 
12
12
  var goal = null;
13
13
  var backData = {};
@@ -25,14 +25,12 @@ export function registerPage(opts) {
25
25
 
26
26
  let loadPageClass = opts.pageClass ? (() => opts.pageClass) : opts.loadPageClass;
27
27
  let namedRoutes = opts.namedRoutes || { [opts.name]: opts.route };
28
- let auth = opts.auth || null;
29
28
  let cacheMarkupBy = opts.cacheMarkupBy || 'path'
30
29
  let extra = opts.extra || {}
31
30
 
32
31
  for (const [name, route] of Object.entries(namedRoutes)) {
33
32
  router.addRoute(name, route, {
34
33
  route,
35
- auth,
36
34
  loadPageClass,
37
35
  loadMarkup: opts.loadMarkup,
38
36
  cacheMarkupBy,
@@ -67,8 +65,8 @@ function emitUrlChanged(url)
67
65
  function initLoading()
68
66
  {
69
67
  var url = router.interpolate(options.loadingPageName, {});
70
- var route = router.parse(url);
71
- return loadPage(route, {});
68
+ var routeResult = router.parse(url);
69
+ return loadPage(routeResult, {});
72
70
  }
73
71
 
74
72
  function showLoading() {
@@ -222,9 +220,19 @@ async function doUpdate(page, data) {
222
220
  document.title = page.title
223
221
  }
224
222
 
225
- function handleHistoryAction(event, url, data, page) {
223
+ function getNewUid()
224
+ {
225
+ let uid = -1;
226
+
227
+ for(let i = 0; i < stack.length; i++)
228
+ uid = Math.max(uid, stack[i].uid)
229
+
230
+ return uid + 1;
231
+ }
232
+
233
+ async function handleHistoryAction(event, url, data, page) {
226
234
  if (event.action == 'push') {
227
- let newUid = stack[stackPointer].uid + 1;
235
+ let newUid = getNewUid()
228
236
 
229
237
  window.history.pushState({ uid: newUid }, null, url);
230
238
  emitUrlChanged(url);
@@ -235,19 +243,24 @@ function handleHistoryAction(event, url, data, page) {
235
243
  stack.push({ uid: newUid, data, page });
236
244
  stackPointer++;
237
245
  }
238
- else if (event.action == 'replace') {
239
- // TODO: this case may be buggy
240
-
241
- // BUG: you can replace the current state with the same url as the previous state, which shouldnt be allowed,
242
- // you cant have the same url in the history twice (next to each other).
243
- // Update this to check for such a case and handle it
244
-
245
- let currentUid = stack[stackPointer].uid;
246
- window.history.replaceState({ uid: currentUid }, null, url);
247
- emitUrlChanged(url);
246
+ else if (event.action == 'replace')
247
+ {
248
+ if(data.route.url == stack[stackPointer - 1]?.data?.route?.url)
249
+ {
250
+ let statesToKeep = [];
251
+ for (var i = 0; i < stackPointer - 1; i++)
252
+ statesToKeep.push(stack[i]);
253
+ statesToKeep.push({ uid: stack[stackPointer - 1].uid, data, page });
248
254
 
249
- stack.pop();
250
- stack.push({ uid: currentUid, data, page });
255
+ await modifyHistory(statesToKeep);
256
+ }
257
+ else
258
+ {
259
+ let newId = getNewUid();
260
+ window.history.replaceState({ uid: newId }, null, url);
261
+ stack[stackPointer] = { uid: newId, data, page };
262
+ emitUrlChanged(url);
263
+ }
251
264
  }
252
265
  else if(event.action == 'back')
253
266
  {
@@ -299,7 +312,6 @@ export async function init(opts) {
299
312
  }
300
313
 
301
314
  await initLoading();
302
-
303
315
  await showLoading();
304
316
  }
305
317
 
@@ -312,18 +324,18 @@ export async function ready()
312
324
 
313
325
  if (e instanceof PageShowError)
314
326
  {
315
- return showPage(e.url, e.data, { action: 'load', distance: 0 }).then(page => {
327
+ return showPage(e.url, e.data, { action: 'load', distance: 0 }).then(async page => {
316
328
  if(e.action == 'replace')
317
- handleHistoryAction({ action: e.action }, e.url, e.data, page);
329
+ await handleHistoryAction({ action: e.action }, e.url, e.data, page);
318
330
  return page;
319
331
  });
320
332
  }
321
333
  });
322
334
 
323
- function handlePopstate(context, direction, distance) {
335
+ async function handlePopstate(context, direction, distance) {
324
336
 
325
337
  if (manuallyAdjustingHistory) {
326
- manuallyAdjustingHistory(context, { action: direction, distance });
338
+ await manuallyAdjustingHistory(context, { action: direction, distance });
327
339
  return;
328
340
  }
329
341
 
@@ -343,7 +355,7 @@ export async function ready()
343
355
  })
344
356
  }
345
357
 
346
- function handleBeforeHidePart1() {
358
+ function handleBeforeHidePart1(lastNavDirection) {
347
359
  // if we're ignoring beforeHide this navigation
348
360
  if (handlingBeforeHide === 'ignore') {
349
361
  handlingBeforeHide = false;
@@ -352,6 +364,8 @@ export async function ready()
352
364
 
353
365
  // if we have a before-unload confirm to show
354
366
  if (stack[stackPointer].page.beforeHide && options.beforeHide && handlingBeforeHide === false) {
367
+ beforeHideNavDirection = lastNavDirection;
368
+
355
369
  var interrupt = stack[stackPointer].page.beforeHide();
356
370
  if (interrupt) {
357
371
  handlingBeforeHide = 'step1';
@@ -359,9 +373,9 @@ export async function ready()
359
373
  // do this in a new thread, you cant call history actions from inside a history-aciton-handler
360
374
  window.setTimeout(() => {
361
375
  // undo the navigation so the URL remains correct whilst we show the confirm dialog
362
- if (lastNavigationDirection == 'fwd')
376
+ if (beforeHideNavDirection == 'fwd')
363
377
  history.back();
364
- else if (lastNavigationDirection == 'back')
378
+ else if (beforeHideNavDirection == 'back')
365
379
  history.forward();
366
380
  }, 1);
367
381
 
@@ -371,7 +385,10 @@ export async function ready()
371
385
 
372
386
  // we've finished beforeHiding
373
387
  if (handlingBeforeHide === 'step2')
388
+ {
374
389
  handlingBeforeHide = false;
390
+ beforeHideNavDirection = null;
391
+ }
375
392
 
376
393
  return false;
377
394
  }
@@ -380,20 +397,26 @@ export async function ready()
380
397
  if (handlingBeforeHide !== 'step1')
381
398
  return false;
382
399
 
400
+ if(manuallyAdjustingHistory)
401
+ return false;
402
+
383
403
  // do the beforeHide action, then...
384
404
  options.beforeHide(stack[stackPointer].page.beforeHide()).then(result => {
385
405
 
386
406
  // if the user confirmed, redo the original action
387
- if (result) {
388
-
407
+ if (result)
408
+ {
389
409
  handlingBeforeHide = 'step2';
390
410
 
391
- if (lastNavigationDirection == 'fwd')
411
+ if (beforeHideNavDirection == 'fwd')
392
412
  history.forward();
393
- else if (lastNavigationDirection == 'back')
413
+ else if (beforeHideNavDirection == 'back')
394
414
  history.back();
395
- } else {
415
+ }
416
+ else
417
+ {
396
418
  handlingBeforeHide = false;
419
+ beforeHideNavDirection = null;
397
420
  }
398
421
  });
399
422
 
@@ -401,23 +424,23 @@ export async function ready()
401
424
  }
402
425
 
403
426
  // listen for browser navigations
404
- window.addEventListener("popstate", e => {
405
- var interrupted = handleBeforeHidePart2();
406
- if (interrupted)
407
- return;
408
-
427
+ window.addEventListener("popstate", async e => {
428
+ // TODO: is 0 sensible here, or should we call getNewId()?
409
429
  let newUid = e.state?.uid || 0;
410
430
  let previousUid = stack[stackPointer].uid;
411
-
412
- lastNavigationDirection = newUid > previousUid ? 'fwd' : 'back';
431
+ let lastNavigationDirection = newUid > previousUid ? 'fwd' : 'back';
413
432
  let distance = Math.abs(newUid - previousUid);
414
433
 
415
- var interrupted = handleBeforeHidePart1();
434
+ var interrupted = handleBeforeHidePart2();
435
+ if (interrupted)
436
+ return;
437
+
438
+ var interrupted = handleBeforeHidePart1(lastNavigationDirection);
416
439
  if (interrupted)
417
440
  return;
418
441
 
419
442
  var context = findContext(newUid);
420
- handlePopstate(context, lastNavigationDirection, distance);
443
+ await handlePopstate(context, lastNavigationDirection, distance);
421
444
  });
422
445
 
423
446
  if(storageAvailable())
@@ -505,30 +528,37 @@ export function printStack() {
505
528
  console.log(stack[i]);
506
529
  }
507
530
 
508
- export function removeHistory(predicate)
531
+ function modifyHistory(statesToKeep)
509
532
  {
510
- let statesToKeep = [];
511
- for (var i = 0; i < stack.length; i++)
512
- {
513
- if (!predicate(stack[i], i))
514
- statesToKeep.push(stack[i]);
515
- }
516
-
517
- // TODO: ensure we always have at least 1 state to keep - must/can this always be the current page?
533
+ // BUG: You can't remove the 2nd stack frame. This is because you'd have to go back to -1, to then go forward to 0 (removing frame 1)
534
+ // For clarity, you can remove (replace) frame 0 (1st), and frame 2+ (3rd+)
535
+ // As a workaround, we replace the 2nd stack frame, if it is the tail, with the root "/".
536
+ // This at least hopefully removes any meaning from it, simply taking you "home" rather than to wherever you were likely trying to remove.
518
537
 
519
538
  if (statesToKeep.length == stack.length)
520
539
  return Promise.resolve();
521
540
 
541
+ return new Promise(async (resolve, reject) => {
522
542
 
523
- return new Promise((resolve, reject) => {
524
-
525
-
526
- let backsToDo = stackPointer - 1;
527
- let currentUid = -1;
543
+ let backsToDo = stackPointer;
528
544
 
529
545
  // TODO: handle stack pointer not being at the tail when this process starts
546
+
547
+ // see BUG comments at the top of this method
548
+ let apply2ndFrameHack = statesToKeep.length == 1 && stack.length > 1
549
+ let secondFrameHackUid = -1
550
+ let defaultPageRouteResult = null;
551
+ let defaultPage = null;
552
+ if(apply2ndFrameHack)
553
+ {
554
+ let url = getPath(options.defaultPageName);
555
+ defaultPageRouteResult = router.parse(url);
556
+ defaultPage = await loadPage(defaultPageRouteResult, {});
557
+
558
+ secondFrameHackUid = getNewUid();
559
+ }
530
560
 
531
- manuallyAdjustingHistory = _ => {
561
+ manuallyAdjustingHistory = async _ => {
532
562
  // rewind to the first history position
533
563
  if(backsToDo > 0)
534
564
  {
@@ -540,36 +570,78 @@ export function removeHistory(predicate)
540
570
  }
541
571
 
542
572
  // reset the stack
573
+
543
574
  stack = [];
544
575
 
545
576
  for (var k = 0; k < statesToKeep.length; k++) {
546
577
  let currentState = statesToKeep[k];
547
- currentState.uid = ++currentUid;
548
578
 
549
579
  if (k == 0)
550
- window.history.replaceState({ uid: currentState.uid }, null, currentState.data.route.url);
580
+ window.history.replaceState({ uid: currentState.uid }, currentState.page.title, currentState.data.route.url);
551
581
  else
552
- window.history.pushState({ uid: currentState.uid }, null, currentState.data.route.url);
582
+ window.history.pushState({ uid: currentState.uid }, currentState.page.title, currentState.data.route.url);
553
583
 
554
- window.dispatchEvent(new CustomEvent('page-manager.url-changed', {
555
- url: currentState.data.route.url
556
- }))
557
-
558
- // TODO: this doesnt seem to work when k=0
584
+ // TODO: this doesnt seem to work when k=0
559
585
  document.title = currentState.page.title;
560
586
 
561
587
  stack.push(currentState);
562
- }
563
588
 
564
- stackPointer = stack.length - 1;
589
+ emitUrlChanged(currentState.data.route.url)
590
+ }
565
591
 
592
+ // see BUG comments at the top of this method
593
+ if(apply2ndFrameHack)
594
+ {
595
+ stack.push({
596
+ uid: secondFrameHackUid,
597
+ data: {
598
+ route: {
599
+ url: defaultPageRouteResult.path,
600
+ path: defaultPageRouteResult.path,
601
+ name: defaultPageRouteResult.name,
602
+ params: defaultPageRouteResult.params
603
+ }
604
+ },
605
+ page: defaultPage
606
+ })
607
+
608
+ stackPointer = 0;
609
+ }
610
+ else
611
+ {
612
+ stackPointer = stack.length - 1;
613
+ }
614
+
566
615
  manuallyAdjustingHistory = false;
616
+
617
+ resolve();
567
618
  };
568
619
 
620
+ // see BUG comments at the top of this method
621
+ if(apply2ndFrameHack && backsToDo == 1)
622
+ {
623
+ window.history.replaceState({ uid: secondFrameHackUid }, defaultPage.title, defaultPageRouteResult.path);
624
+ document.title = defaultPage.title;
625
+ }
626
+
627
+ backsToDo--;
569
628
  history.back();
570
629
  });
571
630
  }
572
631
 
632
+
633
+ export function removeHistory(predicate)
634
+ {
635
+ let statesToKeep = [];
636
+ for (var i = 0; i < stack.length; i++)
637
+ {
638
+ if (!predicate(stack[i], i))
639
+ statesToKeep.push(stack[i]);
640
+ }
641
+
642
+ return modifyHistory(statesToKeep);
643
+ }
644
+
573
645
  export function purgeCache() {
574
646
  for (const key in pageCache)
575
647
  {
@@ -0,0 +1,20 @@
1
+ <html>
2
+ <head>
3
+ <script defer src="/main.js"></script></head>
4
+ <body style="height: 100%;">
5
+ <div style="position: fixed; top: 0; background-color: white;">
6
+ <a href="/page1">Page 1</a>
7
+ <a href="/page2">Page 2</a>
8
+ <a href="/page3">Page 3</a>
9
+ <a href="/page4">Page 4</a>
10
+ <a href="/page/A">Page A</a>
11
+ <a href="/page/B">Page B</a>
12
+ <a href="/404">404</a>
13
+ <a href="/dont-exist">I dont exist</a>
14
+ <a href="/show-fail">I fail to show</a>
15
+ </div>
16
+ <div>
17
+ <button class="btnStack">Print stack</button>
18
+ </div>
19
+ </body>
20
+ </html>
@@ -0,0 +1,3 @@
1
+ <div>
2
+ Loading
3
+ </div>