1
1
// Unlike setInnerHtml, this patches the Dom in place
2
2
function setHtml ( elm , html ) {
3
- window . dispatchEvent ( new Event ( ' EMABeforeMorphDOM' ) ) ;
4
- Idiomorph . morph ( elm , html ) ;
5
- window . dispatchEvent ( new Event ( ' EMABeforeScriptReload' ) ) ;
6
- // Re-add <script> tags, because just DOM diff applying is not enough.
7
- reloadScripts ( elm ) ;
8
- window . dispatchEvent ( new Event ( ' EMAHotReload' ) ) ;
9
- } ;
3
+ window . dispatchEvent ( new Event ( " EMABeforeMorphDOM" ) ) ;
4
+ Idiomorph . morph ( elm , html ) ;
5
+ window . dispatchEvent ( new Event ( " EMABeforeScriptReload" ) ) ;
6
+ // Re-add <script> tags, because just DOM diff applying is not enough.
7
+ reloadScripts ( elm ) ;
8
+ window . dispatchEvent ( new Event ( " EMAHotReload" ) ) ;
9
+ }
10
10
11
11
// FIXME: This doesn't reliably work across all JS.
12
12
// See also the HACK below in one of the invocations.
13
13
function reloadScripts ( elm ) {
14
- Array . from ( elm . querySelectorAll ( "script" ) ) . forEach ( oldScript => {
15
- const newScript = document . createElement ( "script" ) ;
16
- Array . from ( oldScript . attributes )
17
- . forEach ( attr => newScript . setAttribute ( attr . name , attr . value ) ) ;
18
- newScript . appendChild ( document . createTextNode ( oldScript . innerHTML ) ) ;
19
- oldScript . parentNode . replaceChild ( newScript , oldScript ) ;
20
- } ) ;
21
- } ;
14
+ Array . from ( elm . querySelectorAll ( "script" ) ) . forEach ( ( oldScript ) => {
15
+ const newScript = document . createElement ( "script" ) ;
16
+ Array . from ( oldScript . attributes ) . forEach ( ( attr ) =>
17
+ newScript . setAttribute ( attr . name , attr . value ) ,
18
+ ) ;
19
+ newScript . appendChild ( document . createTextNode ( oldScript . innerHTML ) ) ;
20
+ oldScript . parentNode . replaceChild ( newScript , oldScript ) ;
21
+ } ) ;
22
+ }
22
23
23
24
// Ema Status indicator
24
25
const messages = {
25
- connected :"Connected" ,
26
- reloading :"Reloading" ,
27
- connecting :"Connecting to the server" ,
28
- disconnected :"Disconnected - try reloading the window"
26
+ connected :"Connected" ,
27
+ reloading :"Reloading" ,
28
+ connecting :"Connecting to the server" ,
29
+ disconnected :"Disconnected - try reloading the window" ,
29
30
} ;
30
31
function setIndicators ( connected , reloading , connecting , disconnected ) {
31
- const is = { connected, reloading, connecting, disconnected}
32
-
33
- for ( const i in is ) {
34
- document . getElementById ( `ema-${ i } ` ) . style . display =
35
- is [ i ] ?"block" : "none "
36
- if ( is [ i ] )
37
- document . getElementById ( ' ema-message' ) . innerText = messages [ i ]
38
- } ;
39
- document . getElementById ( "ema-indicator" ) . style . display = "block" ;
40
- } ;
41
- window . connected = ( ) => setIndicators ( true , false , false , false )
42
- window . reloading = ( ) => setIndicators ( false , true , false , false )
43
- window . connecting = ( ) => setIndicators ( false , false , true , false )
44
- window . disconnected = ( ) => setIndicators ( false , false , false , true )
32
+ const is = { connected, reloading, connecting, disconnected} ;
33
+
34
+ for ( const i in is ) {
35
+ document . getElementById ( `ema-${ i } ` ) . style . display = is [ i ]
36
+ ?"block"
37
+ : "none" ;
38
+ if ( is [ i ] ) document . getElementById ( " ema-message" ) . innerText = messages [ i ] ;
39
+ }
40
+ document . getElementById ( "ema-indicator" ) . style . display = "block" ;
41
+ }
42
+ window . connected = ( ) => setIndicators ( true , false , false , false ) ;
43
+ window . reloading = ( ) => setIndicators ( false , true , false , false ) ;
44
+ window . connecting = ( ) => setIndicators ( false , false , true , false ) ;
45
+ window . disconnected = ( ) => setIndicators ( false , false , false , true ) ;
45
46
window . hideIndicator = ( ) => {
46
- document . getElementById ( "ema-indicator" ) . style . display = "none" ;
47
+ document . getElementById ( "ema-indicator" ) . style . display = "none" ;
47
48
} ;
48
49
49
50
// Base URL path - for when the ema site isn't served at "/"
@@ -56,133 +57,144 @@ const wsUrl = wsProto + window.location.host + basePath;
56
57
57
58
// WebSocket logic: watching for server changes & route switching
58
59
function init ( reconnecting ) {
59
- // The route current DOM is displaying
60
- let routeVisible = document . location . pathname ;
61
-
62
- const verb = reconnecting ?"Reopening" :"Opening" ;
63
- console . log ( `ema:${ verb } conn${ wsUrl } ...` ) ;
64
- window . connecting ( ) ;
65
- let ws = new WebSocket ( wsUrl ) ;
66
-
67
- function sendObservePath ( path ) {
68
- const relPath = path . startsWith ( basePath ) ?path . slice ( basePath . length ) :path ;
69
- console . debug ( `ema: requesting${ relPath } ` ) ;
70
- ws . send ( relPath ) ;
60
+ // The route current DOM is displaying
61
+ let routeVisible = document . location . pathname ;
62
+
63
+ const verb = reconnecting ?"Reopening" :"Opening" ;
64
+ console . log ( `ema:${ verb } conn${ wsUrl } ...` ) ;
65
+ window . connecting ( ) ;
66
+ let ws = new WebSocket ( wsUrl ) ;
67
+
68
+ function sendObservePath ( path ) {
69
+ const relPath = path . startsWith ( basePath )
70
+ ?path . slice ( basePath . length )
71
+ :path ;
72
+ console . debug ( `ema: requesting${ relPath } ` ) ;
73
+ ws . send ( relPath ) ;
74
+ }
75
+
76
+ // Call this, then the server will send update *once*. Call again for
77
+ // continous monitoring.
78
+ function watchCurrentRoute ( ) {
79
+ console . log ( `ema: ⏿ Observing changes to${ document . location . pathname } ` ) ;
80
+ sendObservePath ( document . location . pathname ) ;
81
+ }
82
+
83
+ function switchRoute ( path , hash = "" ) {
84
+ console . log ( `ema: → Switching to${ path + hash } ` ) ;
85
+ window . history . pushState ( { } , "" , path + hash ) ;
86
+ sendObservePath ( path ) ;
87
+ }
88
+
89
+ function scrollToAnchor ( hash ) {
90
+ console . log ( `ema: Scroll to${ hash } ` ) ;
91
+ var el = document . querySelector ( hash ) ;
92
+ if ( el !== null ) {
93
+ el . scrollIntoView ( { behavior :"smooth" } ) ;
71
94
}
72
-
73
- // Call this, then the server will send update *once*. Call again for
74
- // continous monitoring.
75
- function watchCurrentRoute ( ) {
76
- console . log ( `ema: ⏿ Observing changes to${ document . location . pathname } ` ) ;
77
- sendObservePath ( document . location . pathname ) ;
78
- } ;
79
-
80
- function switchRoute ( path , hash = "" ) {
81
- console . log ( `ema: → Switching to${ path + hash } ` ) ;
82
- window . history . pushState ( { } , "" , path + hash ) ;
83
- sendObservePath ( path ) ;
84
- }
85
-
86
- function scrollToAnchor ( hash ) {
87
- console . log ( `ema: Scroll to${ hash } ` )
88
- var el = document . querySelector ( hash ) ;
89
- if ( el !== null ) {
90
- el . scrollIntoView ( { behavior :'smooth' } ) ;
95
+ }
96
+
97
+ function getAnchorIfOnPage ( linkElement ) {
98
+ const url = new URL ( linkElement . href ) ; // Use URL API for parsing
99
+ return url . host === window . location . host &&
100
+ url . pathname === window . location . pathname &&
101
+ url . hash
102
+ ?url . hash . slice ( 1 ) // Return anchor name (slice off '#')
103
+ :null ; // Not an anchor on the current page
104
+ }
105
+
106
+ function handleRouteClicks ( e ) {
107
+ const origin = e . target . closest ( "a" ) ;
108
+ if ( origin ) {
109
+ if (
110
+ window . location . host === origin . host &&
111
+ origin . getAttribute ( "target" ) != "_blank"
112
+ ) {
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 ) ;
118
+ e . preventDefault ( ) ;
119
+ } else {
120
+ // Switching to another route
121
+ switchRoute ( origin . pathname , origin . hash ) ;
122
+ e . preventDefault ( ) ;
91
123
}
92
- } ;
93
-
94
- function getAnchorIfOnPage ( linkElement ) {
95
- const url = new URL ( linkElement . href ) ; // Use URL API for parsing
96
- return ( url . host === window . location . host && url . pathname === window . location . pathname && url . hash )
97
- ?url . hash . slice ( 1 ) // Return anchor name (slice off '#')
98
- :null ; // Not an anchor on the current page
124
+ }
99
125
}
100
-
101
- function handleRouteClicks ( e ) {
102
- const origin = e . target . closest ( "a" ) ;
103
- if ( origin ) {
104
- if ( window . location . host === origin . host && origin . getAttribute ( "target" ) != "_blank" ) {
105
- let anchor = getAnchorIfOnPage ( origin ) ;
106
- if ( anchor !== null ) {
107
- // Switching to local anchor
108
- window . history . pushState ( { } , "" , origin . href ) ;
109
- scrollToAnchor ( window . location . hash ) ;
110
- e . preventDefault ( ) ;
111
- } else {
112
- // Switching to another route
113
- switchRoute ( origin . pathname , origin . hash ) ;
114
- e . preventDefault ( ) ;
115
- }
116
- } ;
117
- }
118
- } ;
119
- // Intercept route click events, and ask server for its HTML whilst
120
- // managing history state.
121
- window . addEventListener ( `click` , handleRouteClicks ) ;
122
-
123
- ws . onopen = ( ) => {
124
- console . log ( `ema: ... connected!` ) ;
125
- // window.connected();
126
- window . hideIndicator ( ) ;
127
- if ( ! reconnecting ) {
128
- // HACK: We have to reload <script>'s here on initial page load
129
- // here, so as to make Twind continue to function on the *next*
130
- // route change. This is not a problem with *subsequent* (ie. 2nd
131
- // or latter) route clicks, because those have already called
132
- // reloadScripts at least once.
133
- reloadScripts ( document . documentElement ) ;
134
- } ;
135
- watchCurrentRoute ( ) ;
136
- } ;
137
-
138
- ws . onclose = ( ) => {
139
- console . log ( "ema: reconnecting .." ) ;
140
- window . removeEventListener ( `click` , handleRouteClicks ) ;
141
- window . reloading ( ) ;
142
- // Reconnect after as small a time is possible, then retry again.
143
- // ghcid can take 1s or more to reboot. So ideally we need an
144
- // exponential retry logic.
145
- //
146
- // Note that a slow delay (200ms) may often cause websocket
147
- // connection error (ghcid hasn't rebooted yet), which cannot be
148
- // avoided as it is impossible to trap this error and handle it.
149
- // You'll see a big ugly error in the console.
150
- setTimeout ( function ( ) { init ( true ) ; } , 400 ) ;
151
- } ;
152
-
153
-
154
-
155
- ws . onmessage = evt => {
156
- if ( evt . data . startsWith ( "REDIRECT " ) ) {
157
- console . log ( "ema: redirect" ) ;
158
- document . location . href = evt . data . slice ( "REDIRECT " . length ) ;
159
- } else if ( evt . data . startsWith ( "SWITCH " ) ) {
160
- console . log ( "ema: switch" ) ;
161
- switchRoute ( evt . data . slice ( "SWITCH " . length ) ) ;
162
- } else {
163
- console . log ( "ema: ✍ Patching DOM" ) ;
164
- setHtml ( document . documentElement , evt . data ) ;
165
- if ( routeVisible != document . location . pathname ) {
166
- // This is a new route switch; scroll up.
167
- window . scrollTo ( { top :0 } ) ;
168
- routeVisible = document . location . pathname ;
169
- }
170
- if ( window . location . hash ) {
171
- scrollToAnchor ( window . location . hash ) ;
172
- }
173
- } ;
174
- } ;
175
- window . onbeforeunload = evt => { ws . close ( ) ; } ;
176
- window . onpagehide = evt => { ws . close ( ) ; } ;
177
-
178
- // When the user clicks the back button, resume watching the URL in
179
- // the addressback, which has the effect of loading it immediately.
180
- window . onpopstate = function ( e ) {
181
- watchCurrentRoute ( ) ;
182
- } ;
183
-
184
- // API for user invocations
185
- window . ema = {
186
- switchRoute :switchRoute
187
- } ;
188
- } ;
126
+ }
127
+ // Intercept route click events, and ask server for its HTML whilst
128
+ // managing history state.
129
+ window . addEventListener ( `click` , handleRouteClicks ) ;
130
+
131
+ ws . onopen = ( ) => {
132
+ console . log ( `ema: ... connected!` ) ;
133
+ // window.connected();
134
+ window . hideIndicator ( ) ;
135
+ if ( ! reconnecting ) {
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 ) ;
142
+ }
143
+ watchCurrentRoute ( ) ;
144
+ } ;
145
+
146
+ ws . onclose = ( ) => {
147
+ console . log ( "ema: reconnecting .." ) ;
148
+ window . removeEventListener ( `click` , handleRouteClicks ) ;
149
+ window . reloading ( ) ;
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.
153
+ //
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 ( ) {
159
+ init ( true ) ;
160
+ } , 400 ) ;
161
+ } ;
162
+
163
+ ws . onmessage = ( evt ) => {
164
+ if ( evt . data . startsWith ( "REDIRECT " ) ) {
165
+ console . log ( "ema: redirect" ) ;
166
+ document . location . href = evt . data . slice ( "REDIRECT " . length ) ;
167
+ } else if ( evt . data . startsWith ( "SWITCH " ) ) {
168
+ console . log ( "ema: switch" ) ;
169
+ switchRoute ( evt . data . slice ( "SWITCH " . length ) ) ;
170
+ } else {
171
+ console . log ( "ema: ✍ 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 ;
177
+ }
178
+ if ( window . location . hash ) {
179
+ scrollToAnchor ( window . location . hash ) ;
180
+ }
181
+ }
182
+ } ;
183
+ window . onbeforeunload = ( evt ) => {
184
+ ws . close ( ) ;
185
+ } ;
186
+ window . onpagehide = ( evt ) => {
187
+ ws . close ( ) ;
188
+ } ;
189
+
190
+ // When the user clicks the back button, resume watching the URL in
191
+ // the addressback, which has the effect of loading it immediately.
192
+ window . onpopstate = function ( e ) {
193
+ watchCurrentRoute ( ) ;
194
+ } ;
195
+
196
+ // API for user invocations
197
+ window . ema = {
198
+ switchRoute :switchRoute ,
199
+ } ;
200
+ }