_navigation.py
1 # SPDX-License-Identifier: MIT 2 # 3 # Copyright (c) 2021 The Anvil Extras project team members listed at 4 # https://github.com/anvilistas/anvil-extras/graphs/contributors 5 # 6 # This software is published at https://github.com/anvilistas/anvil-extras 7 8 from functools import wraps 9 10 from anvil.js.window import history, location, window 11 12 from . import _router 13 14 __version__ = "3.1.0" 15 16 # re-initialise the state object which was overridden on load or this is a new session 17 state = history.state or {"url": location.hash, "pos": 0} 18 if "url" not in state: 19 state = {"url": location.hash, "pos": 0} 20 history.replaceState(state, "", state["url"]) 21 22 # undo and pos are used for unload behavior 23 current = {"undo": 0, "pos": state["pos"]} 24 25 # Form Unload Behaviour - here we prevent the user from navigating away from the current form 26 # while we wait for the unload function to complete 27 28 undoing = False 29 waiting = False 30 31 32 # when the window back or forward button is pressed onPopState is triggered 33 # a popstate is also triggered from a change in the URL in this case the window.history.state will be None 34 def onPopState(e): 35 global undoing, waiting 36 if undoing: 37 undoing = False 38 current["pos"] = history.state["pos"] 39 return 40 elif waiting: 41 return preventUnloadPopState(e) 42 43 state = e.state 44 if state: # then we're loading from back forward navigation 45 current["undo"] = current["pos"] - state["pos"] 46 current["pos"] = state["pos"] 47 else: 48 current["undo"] = -1 49 current["pos"] += 1 50 state = {"url": location.hash, "pos": current["pos"]} 51 52 history.replaceState(state, "", state["url"]) 53 # we always favour the state['url'] over location.hash 54 # since we allow (replace_current_url=True, set_in_history=False) 55 56 _router.navigate() 57 58 59 window.onpopstate = onPopState 60 61 62 def stopUnload(): 63 global undoing 64 undoing = True 65 history.go(current["undo"]) 66 67 68 def preventUnloadPopState(e): 69 global undoing 70 e.preventDefault() 71 state = e.state 72 if state: 73 undoing = True 74 history.go(current["pos"] - state["pos"]) # reverse the navigation 75 else: 76 # the user is determined to navigate away and has changed the url manually so let them! 77 # Not letting them will break the app... 78 current["pos"] += 1 79 state = {"url": location.hash, "pos": current["pos"]} 80 history.replaceState(state, "") 81 window.onbeforeunload = None 82 location.reload() 83 84 85 class PreventUnloading: 86 def __enter__(self): 87 global waiting 88 waiting = True 89 90 def __exit__(self, *args): 91 global waiting 92 waiting = False 93 94 95 def ensure_hash(f): 96 @wraps(f) 97 def hash_wrapper(url): 98 if not url.startswith("#"): 99 url = "#" + url 100 return f(url) 101 102 return hash_wrapper 103 104 105 @ensure_hash 106 def pushState(url): 107 # set_in_history = True, replace_current_url=False 108 current["pos"] += 1 109 current["undo"] = -1 110 state = {"url": url, "pos": current["pos"]} 111 history.pushState(state, "", url) 112 113 114 @ensure_hash 115 def replaceState(url): 116 # set_in_history=True, replace_current_url=True 117 current["undo"] = 0 118 state = {"url": url, "pos": history.state["pos"]} 119 history.replaceState(state, "", url) 120 121 122 @ensure_hash 123 def replaceUrlNotState(url): 124 # set_in_history=False, replace_current_url=True 125 current["undo"] = 0 126 history.replaceState(history.state, "", url)