]>
Git — Sourcephile - haskell/literate-web.git/blob - www/live-shim.js
1 function htmlToElem(html
) {
2 let temp
= document
.createElement('template');
3 html
= html
.trim(); // Never return a space text node as a result
5 return temp
.content
.firstChild
;
8 // Unlike setInnerHtml, this patches the Dom in place
9 function setHtml(elm
, html
) {
10 var htmlElem
= htmlToElem(html
);
11 window
.dispatchEvent(new Event('LiveBeforeMorphDOM'));
13 window
.dispatchEvent(new Event('LiveBeforeScriptReload'));
14 // Re-add <script> tags, because just DOM diff applying is not enough.
16 window
.dispatchEvent(new Event('LiveHotReload'));
19 // FIXME: This doesn't reliably work across all JS.
20 // See also the HACK below in one of the invocations.
21 function reloadScripts(elm
) {
22 Array
.from(elm
.querySelectorAll("script")).forEach(oldScript
=> {
23 const newScript
= document
.createElement("script");
24 Array
.from(oldScript
.attributes
)
25 .forEach(attr
=> newScript
.setAttribute(attr
.name
, attr
.value
));
26 newScript
.appendChild(document
.createTextNode(oldScript
.innerHTML
));
27 oldScript
.parentNode
.replaceChild(newScript
, oldScript
);
31 // Live Status indicator
33 connected: "Connected",
34 reloading: "Reloading",
35 connecting: "Connecting to the server",
36 disconnected: "Disconnected - try reloading the window"
38 function setIndicators(connected
, reloading
, connecting
, disconnected
) {
39 const is
= { connected
, reloading
, connecting
, disconnected
}
42 document
.getElementById(`live-${i}`).style
.display
=
43 is
[i
] ? "block" : "none"
45 document
.getElementById('live-message').innerText
= messages
[i
]
47 document
.getElementById("live-indicator").style
.display
= "block";
49 window
.connected
= () => setIndicators(true, false, false, false)
50 window
.reloading
= () => setIndicators(false, true, false, false)
51 window
.connecting
= () => setIndicators(false, false, true, false)
52 window
.disconnected
= () => setIndicators(false, false, false, true)
53 window
.hideIndicator
= () => {
54 document
.getElementById("live-indicator").style
.display
= "none";
57 // Base URL path - for when the live site isn't served at "/"
58 const baseHref
= document
.getElementsByTagName("base")[0]?.href
;
59 const basePath
= baseHref
? new URL(baseHref
).pathname : "/";
61 // Use TLS for websocket iff the current page is also served with TLS
62 const wsProto
= window
.location
.protocol
=== "https:" ? "wss://" : "ws://";
63 const wsUrl
= wsProto
+ window
.location
.host
+ basePath
;
65 // WebSocket logic: watching for server changes & route switching
66 function init(reconnecting
) {
67 // The route current DOM is displaying
68 let routeVisible
= document
.location
.pathname
;
70 const verb
= reconnecting
? "Reopening" : "Opening";
71 console
.log(`live: ${verb} conn ${wsUrl} ...`);
73 let ws
= new WebSocket(wsUrl
);
75 function sendObservePath(path
) {
76 const relPath
= path
.startsWith(basePath
) ? path
.slice(basePath
.length
) : path
;
77 console
.debug(`live: requesting ${relPath}`);
81 // Call this, then the server will send update *once*. Call again for
82 // continous monitoring.
83 function watchCurrentRoute() {
84 console
.log(`live: ⏿ Observing changes to ${document.location.pathname}`);
85 sendObservePath(document
.location
.pathname
);
88 function switchRoute(path
, hash
= "") {
89 console
.log(`live: → Switching to ${path + hash}`);
90 window
.history
.pushState({}, "", path
+ hash
);
91 sendObservePath(path
);
94 function scrollToAnchor(hash
) {
95 console
.log(`live: Scroll to ${hash}`)
96 var el
= document
.querySelector(hash
);
98 el
.scrollIntoView({ behavior: 'smooth' });
102 function getAnchorIfOnPage(linkElement
) {
103 const url
= new URL(linkElement
.href
); // Use URL API for parsing
104 return (url
.host
=== window
.location
.host
&& url
.pathname
=== window
.location
.pathname
&& url
.hash
)
105 ? url
.hash
.slice(1) // Return anchor name (slice off '#')
106 : null; // Not an anchor on the current page
109 function handleRouteClicks(e
) {
110 const origin
= e
.target
.closest("a");
112 if (window
.location
.host
=== origin
.host
&& origin
.getAttribute("target") != "_blank") {
113 let anchor
= getAnchorIfOnPage(origin
);
114 if (anchor
!== null) {
115 // Switching to local anchor
116 window
.history
.pushState({}, "", origin
.href
);
117 scrollToAnchor(window
.location
.hash
);
120 // Switching to another route
121 switchRoute(origin
.pathname
, origin
.hash
);
127 // Intercept route click events, and ask server for its HTML whilst
128 // managing history state.
129 window
.addEventListener(`click`, handleRouteClicks
);
132 console
.log(`live: ... connected!`);
133 // window.connected();
134 window
.hideIndicator();
136 // HACK: We have to reload <script>'s here on initial page load
137 // here, so as to make Twind continue to function on the *next*
138 // route change. This is not a problem with *subsequent* (ie. 2nd
139 // or latter) route clicks, because those have already called
140 // reloadScripts at least once.
141 reloadScripts(document
.documentElement
);
147 console
.log("live: reconnecting ..");
148 window
.removeEventListener(`click`, handleRouteClicks
);
150 // Reconnect after as small a time is possible, then retry again.
151 // ghcid can take 1s or more to reboot. So ideally we need an
152 // exponential retry logic.
154 // Note that a slow delay (200ms) may often cause websocket
155 // connection error (ghcid hasn't rebooted yet), which cannot be
156 // avoided as it is impossible to trap this error and handle it.
157 // You'll see a big ugly error in the console.
158 setTimeout(function () { init(true); }, 400);
163 ws
.onmessage
= evt
=> {
164 if (evt
.data
.startsWith("REDIRECT ")) {
165 console
.log("live: redirect");
166 document
.location
.href
= evt
.data
.slice("REDIRECT ".length
);
167 } else if (evt
.data
.startsWith("SWITCH ")) {
168 console
.log("live: switch");
169 switchRoute(evt
.data
.slice("SWITCH ".length
));
171 console
.log("live: ✍ Patching DOM");
172 setHtml(document
.documentElement
, evt
.data
);
173 if (routeVisible
!= document
.location
.pathname
) {
174 // This is a new route switch; scroll up.
175 window
.scrollTo({ top: 0 });
176 routeVisible
= document
.location
.pathname
;
178 if (window
.location
.hash
) {
179 scrollToAnchor(window
.location
.hash
);
183 window
.onbeforeunload
= evt
=> { ws
.close(); };
184 window
.onpagehide
= evt
=> { ws
.close(); };
186 // When the user clicks the back button, resume watching the URL in
187 // the addressback, which has the effect of loading it immediately.
188 window
.onpopstate = function (e
) {
192 // API for user invocations
194 switchRoute: switchRoute