@trullock/page-manager 1.2.0 → 1.3.0

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.0",
3
+ "version": "1.3.0",
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,16 +25,16 @@ 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'
29
+ let extra = opts.extra || {}
30
30
 
31
31
  for (const [name, route] of Object.entries(namedRoutes)) {
32
32
  router.addRoute(name, route, {
33
33
  route,
34
- auth,
35
34
  loadPageClass,
36
35
  loadMarkup: opts.loadMarkup,
37
- cacheMarkupBy
36
+ cacheMarkupBy,
37
+ extra
38
38
  });
39
39
  }
40
40
  }
@@ -65,8 +65,8 @@ function emitUrlChanged(url)
65
65
  function initLoading()
66
66
  {
67
67
  var url = router.interpolate(options.loadingPageName, {});
68
- var route = router.parse(url);
69
- return loadPage(route, {});
68
+ var routeResult = router.parse(url);
69
+ return loadPage(routeResult, {});
70
70
  }
71
71
 
72
72
  function showLoading() {
@@ -220,7 +220,7 @@ async function doUpdate(page, data) {
220
220
  document.title = page.title
221
221
  }
222
222
 
223
- function handleHistoryAction(event, url, data, page) {
223
+ async function handleHistoryAction(event, url, data, page) {
224
224
  if (event.action == 'push') {
225
225
  let newUid = stack[stackPointer].uid + 1;
226
226
 
@@ -233,19 +233,22 @@ function handleHistoryAction(event, url, data, page) {
233
233
  stack.push({ uid: newUid, data, page });
234
234
  stackPointer++;
235
235
  }
236
- else if (event.action == 'replace') {
237
- // TODO: this case may be buggy
238
-
239
- // BUG: you can replace the current state with the same url as the previous state, which shouldnt be allowed,
240
- // you cant have the same url in the history twice (next to each other).
241
- // Update this to check for such a case and handle it
242
-
236
+ else if (event.action == 'replace')
237
+ {
243
238
  let currentUid = stack[stackPointer].uid;
244
239
  window.history.replaceState({ uid: currentUid }, null, url);
245
240
  emitUrlChanged(url);
241
+ stack[stackPointer] = { uid: currentUid, data, page };
242
+
243
+ if(stack[stackPointer].data.route.url == stack[stackPointer - 1]?.data?.route?.url)
244
+ {
245
+ let statesToKeep = [];
246
+ for (var i = 0; i < stackPointer - 1; i++)
247
+ statesToKeep.push(stack[i]);
248
+ statesToKeep.push(stack[stackPointer]);
246
249
 
247
- stack.pop();
248
- stack.push({ uid: currentUid, data, page });
250
+ await modifyHistory(statesToKeep);
251
+ }
249
252
  }
250
253
  else if(event.action == 'back')
251
254
  {
@@ -297,7 +300,6 @@ export async function init(opts) {
297
300
  }
298
301
 
299
302
  await initLoading();
300
-
301
303
  await showLoading();
302
304
  }
303
305
 
@@ -310,18 +312,18 @@ export async function ready()
310
312
 
311
313
  if (e instanceof PageShowError)
312
314
  {
313
- return showPage(e.url, e.data, { action: 'load', distance: 0 }).then(page => {
315
+ return showPage(e.url, e.data, { action: 'load', distance: 0 }).then(async page => {
314
316
  if(e.action == 'replace')
315
- handleHistoryAction({ action: e.action }, e.url, e.data, page);
317
+ await handleHistoryAction({ action: e.action }, e.url, e.data, page);
316
318
  return page;
317
319
  });
318
320
  }
319
321
  });
320
322
 
321
- function handlePopstate(context, direction, distance) {
323
+ async function handlePopstate(context, direction, distance) {
322
324
 
323
325
  if (manuallyAdjustingHistory) {
324
- manuallyAdjustingHistory(context, { action: direction, distance });
326
+ await manuallyAdjustingHistory(context, { action: direction, distance });
325
327
  return;
326
328
  }
327
329
 
@@ -341,7 +343,7 @@ export async function ready()
341
343
  })
342
344
  }
343
345
 
344
- function handleBeforeHidePart1() {
346
+ function handleBeforeHidePart1(lastNavDirection) {
345
347
  // if we're ignoring beforeHide this navigation
346
348
  if (handlingBeforeHide === 'ignore') {
347
349
  handlingBeforeHide = false;
@@ -350,6 +352,8 @@ export async function ready()
350
352
 
351
353
  // if we have a before-unload confirm to show
352
354
  if (stack[stackPointer].page.beforeHide && options.beforeHide && handlingBeforeHide === false) {
355
+ beforeHideNavDirection = lastNavDirection;
356
+
353
357
  var interrupt = stack[stackPointer].page.beforeHide();
354
358
  if (interrupt) {
355
359
  handlingBeforeHide = 'step1';
@@ -357,9 +361,9 @@ export async function ready()
357
361
  // do this in a new thread, you cant call history actions from inside a history-aciton-handler
358
362
  window.setTimeout(() => {
359
363
  // undo the navigation so the URL remains correct whilst we show the confirm dialog
360
- if (lastNavigationDirection == 'fwd')
364
+ if (beforeHideNavDirection == 'fwd')
361
365
  history.back();
362
- else if (lastNavigationDirection == 'back')
366
+ else if (beforeHideNavDirection == 'back')
363
367
  history.forward();
364
368
  }, 1);
365
369
 
@@ -369,7 +373,10 @@ export async function ready()
369
373
 
370
374
  // we've finished beforeHiding
371
375
  if (handlingBeforeHide === 'step2')
376
+ {
372
377
  handlingBeforeHide = false;
378
+ beforeHideNavDirection = null;
379
+ }
373
380
 
374
381
  return false;
375
382
  }
@@ -378,20 +385,26 @@ export async function ready()
378
385
  if (handlingBeforeHide !== 'step1')
379
386
  return false;
380
387
 
388
+ if(manuallyAdjustingHistory)
389
+ return false;
390
+
381
391
  // do the beforeHide action, then...
382
392
  options.beforeHide(stack[stackPointer].page.beforeHide()).then(result => {
383
393
 
384
394
  // if the user confirmed, redo the original action
385
- if (result) {
386
-
395
+ if (result)
396
+ {
387
397
  handlingBeforeHide = 'step2';
388
398
 
389
- if (lastNavigationDirection == 'fwd')
399
+ if (beforeHideNavDirection == 'fwd')
390
400
  history.forward();
391
- else if (lastNavigationDirection == 'back')
401
+ else if (beforeHideNavDirection == 'back')
392
402
  history.back();
393
- } else {
403
+ }
404
+ else
405
+ {
394
406
  handlingBeforeHide = false;
407
+ beforeHideNavDirection = null;
395
408
  }
396
409
  });
397
410
 
@@ -399,23 +412,22 @@ export async function ready()
399
412
  }
400
413
 
401
414
  // listen for browser navigations
402
- window.addEventListener("popstate", e => {
403
- var interrupted = handleBeforeHidePart2();
404
- if (interrupted)
405
- return;
406
-
415
+ window.addEventListener("popstate", async e => {
407
416
  let newUid = e.state?.uid || 0;
408
417
  let previousUid = stack[stackPointer].uid;
409
-
410
- lastNavigationDirection = newUid > previousUid ? 'fwd' : 'back';
418
+ let lastNavigationDirection = newUid > previousUid ? 'fwd' : 'back';
411
419
  let distance = Math.abs(newUid - previousUid);
412
420
 
413
- var interrupted = handleBeforeHidePart1();
421
+ var interrupted = handleBeforeHidePart2();
422
+ if (interrupted)
423
+ return;
424
+
425
+ var interrupted = handleBeforeHidePart1(lastNavigationDirection);
414
426
  if (interrupted)
415
427
  return;
416
428
 
417
429
  var context = findContext(newUid);
418
- handlePopstate(context, lastNavigationDirection, distance);
430
+ await handlePopstate(context, lastNavigationDirection, distance);
419
431
  });
420
432
 
421
433
  if(storageAvailable())
@@ -503,34 +515,46 @@ export function printStack() {
503
515
  console.log(stack[i]);
504
516
  }
505
517
 
506
- export function removeHistory(predicate)
518
+ function modifyHistory(statesToKeep)
507
519
  {
508
- let statesToKeep = [];
509
- for (var i = 0; i < stack.length; i++)
510
- {
511
- if (!predicate(stack[i], i))
512
- statesToKeep.push(stack[i]);
513
- }
514
-
515
- // TODO: ensure we always have at least 1 state to keep - must/can this always be the current page?
520
+ // 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)
521
+ // For clarity, you can remove (replace) frame 0 (1st), and frame 2+ (3rd+)
522
+ // As a workaround, we replace the 2nd stack frame, if it is the tail, with the root "/".
523
+ // This at least hopefully removes any meaning from it, simply taking you "home" rather than to wherever you were likely trying to remove.
516
524
 
517
525
  if (statesToKeep.length == stack.length)
518
526
  return Promise.resolve();
519
527
 
528
+ return new Promise(async (resolve, reject) => {
520
529
 
521
- return new Promise((resolve, reject) => {
522
-
523
-
524
- let backsToDo = stackPointer - 1;
530
+ let backsToDo = stackPointer;
525
531
  let currentUid = -1;
526
532
 
527
533
  // TODO: handle stack pointer not being at the tail when this process starts
534
+
535
+ // see BUG comments at the top of this method
536
+ let apply2ndFrameHack = statesToKeep.length == 1 && stack.length > 1
537
+ let defaultPageRouteResult = null;
538
+ let defaultPage = null;
539
+ if(apply2ndFrameHack)
540
+ {
541
+ let url = getPath(options.defaultPageName);
542
+ defaultPageRouteResult = router.parse(url);
543
+ defaultPage = await loadPage(defaultPageRouteResult, {});
544
+ }
528
545
 
529
- manuallyAdjustingHistory = _ => {
546
+ manuallyAdjustingHistory = async _ => {
530
547
  // rewind to the first history position
531
548
  if(backsToDo > 0)
532
549
  {
533
550
  window.setTimeout(() => {
551
+ // see BUG comments at the top of this method
552
+ if(apply2ndFrameHack && backsToDo == 1)
553
+ {
554
+ window.history.replaceState({ uid: stack[1].uid }, defaultPage.title, defaultPageRouteResult.path);
555
+ document.title = defaultPage.title;
556
+ }
557
+
534
558
  backsToDo--;
535
559
  history.back();
536
560
  }, 1);
@@ -538,6 +562,7 @@ export function removeHistory(predicate)
538
562
  }
539
563
 
540
564
  // reset the stack
565
+
541
566
  stack = [];
542
567
 
543
568
  for (var k = 0; k < statesToKeep.length; k++) {
@@ -545,29 +570,63 @@ export function removeHistory(predicate)
545
570
  currentState.uid = ++currentUid;
546
571
 
547
572
  if (k == 0)
548
- window.history.replaceState({ uid: currentState.uid }, null, currentState.data.route.url);
573
+ window.history.replaceState({ uid: currentState.uid }, currentState.page.title, currentState.data.route.url);
549
574
  else
550
- window.history.pushState({ uid: currentState.uid }, null, currentState.data.route.url);
575
+ window.history.pushState({ uid: currentState.uid }, currentState.page.title, currentState.data.route.url);
551
576
 
552
- window.dispatchEvent(new CustomEvent('page-manager.url-changed', {
553
- url: currentState.data.route.url
554
- }))
577
+ emitUrlChanged(currentState.data.route.url)
555
578
 
556
- // TODO: this doesnt seem to work when k=0
579
+ // TODO: this doesnt seem to work when k=0
557
580
  document.title = currentState.page.title;
558
581
 
559
582
  stack.push(currentState);
560
583
  }
561
584
 
562
- stackPointer = stack.length - 1;
563
-
585
+ // see BUG comments at the top of this method
586
+ if(apply2ndFrameHack)
587
+ {
588
+ stack.push({
589
+ uid: 1,
590
+ data: {
591
+ route: {
592
+ url: defaultPageRouteResult.path,
593
+ path: defaultPageRouteResult.path,
594
+ name: defaultPageRouteResult.name,
595
+ params: defaultPageRouteResult.params
596
+ }
597
+ },
598
+ page: defaultPage
599
+ })
600
+
601
+ stackPointer = 0;
602
+ }
603
+ else
604
+ {
605
+ stackPointer = stack.length - 1;
606
+ }
607
+
564
608
  manuallyAdjustingHistory = false;
609
+
610
+ resolve();
565
611
  };
566
612
 
567
613
  history.back();
568
614
  });
569
615
  }
570
616
 
617
+
618
+ export function removeHistory(predicate)
619
+ {
620
+ let statesToKeep = [];
621
+ for (var i = 0; i < stack.length; i++)
622
+ {
623
+ if (!predicate(stack[i], i))
624
+ statesToKeep.push(stack[i]);
625
+ }
626
+
627
+ return modifyHistory(statesToKeep);
628
+ }
629
+
571
630
  export function purgeCache() {
572
631
  for (const key in pageCache)
573
632
  {
@@ -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>