Fossil SCM

Added optional fossil.fetch() beforesend/aftersend callbacks to allow us to... /efilepage now disables all input elements while AJAX requests are in-flight and updates the page with a 'wait' cursor.

stephan 2020-05-18 05:19 fileedit-ajaxify
Commit 7e197260fd1a797fee9ce4f5a863948f9871d91bc562fced4a2d744addb4550d
--- src/fossil.fetch.js
+++ src/fossil.fetch.js
@@ -22,12 +22,14 @@
2222
- onload: callback(responseData) (default = output response to the
2323
console). In the context of the callback, the options object is
2424
"this", noting that this call may have amended the options object
2525
with state other than what the caller provided.
2626
27
- - onerror: callback(XHR onload event | exception)
28
- (default = event or exception to the console).
27
+ - onerror: callback(XHR onload event | exception) (default = event
28
+ or exception to the console). Triggered if the request generates
29
+ any response other than HTTP 200. In the context of the callback,
30
+ the options object is "this".
2931
3032
- method: 'POST' | 'GET' (default = 'GET'). CASE SENSITIVE!
3133
3234
- payload: anything acceptable by XHR2.send(ARG) (DOMString,
3335
Document, FormData, Blob, File, ArrayBuffer), or a plain object or
@@ -64,17 +66,28 @@
6466
header values. When a map is passed on, all of its keys are
6567
lower-cased. When a given header is requested and that header is
6668
set multiple times, their values are (per the XHR docs)
6769
concatenated together with ", " between them.
6870
69
- When an options object does not provide onload() or onerror()
70
- handlers of its own, this function falls back to
71
- fossil.fetch.onload() and fossil.fetch.onerror() as defaults. The
72
- default implementations route the data through the dev console and
73
- (for onerror()) through fossil.error(). Individual pages may
74
- overwrite those members to provide default implementations suitable
75
- for the page's use.
71
+ - beforesend/aftersend: optional callbacks which are called without
72
+ arguments immediately before the request is submitted and
73
+ immediately after it is received, regardless of success or
74
+ error. In the context of the callback, the options object is the
75
+ "this". These can be used to, e.g., keep track of in-flight
76
+ requests and update the UI accordingly, e.g. disabling/enabling DOM
77
+ elements. Any exceptions triggered by beforesend/aftersend are
78
+ caught and silently ignored.
79
+
80
+ When an options object does not provide
81
+ onload/onerror/beforesend/aftersend handlers of its own, this
82
+ function falls to defaults which are member properties of this
83
+ function with the same name, e.g. fossil.fetch.onload(). The
84
+ default onload/onerror implementations route the data through the
85
+ dev console and (for onerror()) through fossil.error(). The default
86
+ beforesend/aftersend are no-ops. Individual pages may overwrite
87
+ those members to provide default implementations suitable for the
88
+ page's use, e.g. keeping track of how many in-flight
7689
7790
Returns this object, noting that the XHR request is asynchronous,
7891
and still in transit (or has yet to be sent) when that happens.
7992
*/
8093
window.fossil.fetch = function f(uri,opt){
@@ -121,10 +134,12 @@
121134
if('/'===uri[0]) uri = uri.substr(1);
122135
if(!opt) opt = {};
123136
else if('function'===typeof opt) opt={onload:opt};
124137
if(!opt.onload) opt.onload = f.onload;
125138
if(!opt.onerror) opt.onerror = f.onerror;
139
+ if(!opt.beforesend) opt.beforesend = f.beforesend;
140
+ if(!opt.aftersend) opt.aftersend = f.aftersend;
126141
let payload = opt.payload, jsonResponse = false;
127142
if(undefined!==payload){
128143
opt.method = 'POST';
129144
if(!(payload instanceof FormData)
130145
&& !(payload instanceof Document)
@@ -151,10 +166,11 @@
151166
x.responseType = 'text';
152167
}else{
153168
x.responseType = opt.responseType||'text';
154169
}
155170
x.onload = function(e){
171
+ try{opt.aftersend()}catch(e){/*ignore*/}
156172
if(200!==this.status){
157173
opt.onerror(e);
158174
return;
159175
}
160176
const orh = opt.responseHeaders;
@@ -176,9 +192,13 @@
176192
opt.onload.apply(opt, args);
177193
}catch(e){
178194
opt.onerror(e);
179195
}
180196
};
197
+ try{opt.beforesend()}catch(e){/*ignore*/}
181198
if(undefined!==payload) x.send(payload);
182199
else x.send();
183200
return this;
184201
};
202
+
203
+window.fossil.fetch.beforesend = function(){};
204
+window.fossil.fetch.aftersend = function(){};
185205
--- src/fossil.fetch.js
+++ src/fossil.fetch.js
@@ -22,12 +22,14 @@
22 - onload: callback(responseData) (default = output response to the
23 console). In the context of the callback, the options object is
24 "this", noting that this call may have amended the options object
25 with state other than what the caller provided.
26
27 - onerror: callback(XHR onload event | exception)
28 (default = event or exception to the console).
 
 
29
30 - method: 'POST' | 'GET' (default = 'GET'). CASE SENSITIVE!
31
32 - payload: anything acceptable by XHR2.send(ARG) (DOMString,
33 Document, FormData, Blob, File, ArrayBuffer), or a plain object or
@@ -64,17 +66,28 @@
64 header values. When a map is passed on, all of its keys are
65 lower-cased. When a given header is requested and that header is
66 set multiple times, their values are (per the XHR docs)
67 concatenated together with ", " between them.
68
69 When an options object does not provide onload() or onerror()
70 handlers of its own, this function falls back to
71 fossil.fetch.onload() and fossil.fetch.onerror() as defaults. The
72 default implementations route the data through the dev console and
73 (for onerror()) through fossil.error(). Individual pages may
74 overwrite those members to provide default implementations suitable
75 for the page's use.
 
 
 
 
 
 
 
 
 
 
 
76
77 Returns this object, noting that the XHR request is asynchronous,
78 and still in transit (or has yet to be sent) when that happens.
79 */
80 window.fossil.fetch = function f(uri,opt){
@@ -121,10 +134,12 @@
121 if('/'===uri[0]) uri = uri.substr(1);
122 if(!opt) opt = {};
123 else if('function'===typeof opt) opt={onload:opt};
124 if(!opt.onload) opt.onload = f.onload;
125 if(!opt.onerror) opt.onerror = f.onerror;
 
 
126 let payload = opt.payload, jsonResponse = false;
127 if(undefined!==payload){
128 opt.method = 'POST';
129 if(!(payload instanceof FormData)
130 && !(payload instanceof Document)
@@ -151,10 +166,11 @@
151 x.responseType = 'text';
152 }else{
153 x.responseType = opt.responseType||'text';
154 }
155 x.onload = function(e){
 
156 if(200!==this.status){
157 opt.onerror(e);
158 return;
159 }
160 const orh = opt.responseHeaders;
@@ -176,9 +192,13 @@
176 opt.onload.apply(opt, args);
177 }catch(e){
178 opt.onerror(e);
179 }
180 };
 
181 if(undefined!==payload) x.send(payload);
182 else x.send();
183 return this;
184 };
 
 
 
185
--- src/fossil.fetch.js
+++ src/fossil.fetch.js
@@ -22,12 +22,14 @@
22 - onload: callback(responseData) (default = output response to the
23 console). In the context of the callback, the options object is
24 "this", noting that this call may have amended the options object
25 with state other than what the caller provided.
26
27 - onerror: callback(XHR onload event | exception) (default = event
28 or exception to the console). Triggered if the request generates
29 any response other than HTTP 200. In the context of the callback,
30 the options object is "this".
31
32 - method: 'POST' | 'GET' (default = 'GET'). CASE SENSITIVE!
33
34 - payload: anything acceptable by XHR2.send(ARG) (DOMString,
35 Document, FormData, Blob, File, ArrayBuffer), or a plain object or
@@ -64,17 +66,28 @@
66 header values. When a map is passed on, all of its keys are
67 lower-cased. When a given header is requested and that header is
68 set multiple times, their values are (per the XHR docs)
69 concatenated together with ", " between them.
70
71 - beforesend/aftersend: optional callbacks which are called without
72 arguments immediately before the request is submitted and
73 immediately after it is received, regardless of success or
74 error. In the context of the callback, the options object is the
75 "this". These can be used to, e.g., keep track of in-flight
76 requests and update the UI accordingly, e.g. disabling/enabling DOM
77 elements. Any exceptions triggered by beforesend/aftersend are
78 caught and silently ignored.
79
80 When an options object does not provide
81 onload/onerror/beforesend/aftersend handlers of its own, this
82 function falls to defaults which are member properties of this
83 function with the same name, e.g. fossil.fetch.onload(). The
84 default onload/onerror implementations route the data through the
85 dev console and (for onerror()) through fossil.error(). The default
86 beforesend/aftersend are no-ops. Individual pages may overwrite
87 those members to provide default implementations suitable for the
88 page's use, e.g. keeping track of how many in-flight
89
90 Returns this object, noting that the XHR request is asynchronous,
91 and still in transit (or has yet to be sent) when that happens.
92 */
93 window.fossil.fetch = function f(uri,opt){
@@ -121,10 +134,12 @@
134 if('/'===uri[0]) uri = uri.substr(1);
135 if(!opt) opt = {};
136 else if('function'===typeof opt) opt={onload:opt};
137 if(!opt.onload) opt.onload = f.onload;
138 if(!opt.onerror) opt.onerror = f.onerror;
139 if(!opt.beforesend) opt.beforesend = f.beforesend;
140 if(!opt.aftersend) opt.aftersend = f.aftersend;
141 let payload = opt.payload, jsonResponse = false;
142 if(undefined!==payload){
143 opt.method = 'POST';
144 if(!(payload instanceof FormData)
145 && !(payload instanceof Document)
@@ -151,10 +166,11 @@
166 x.responseType = 'text';
167 }else{
168 x.responseType = opt.responseType||'text';
169 }
170 x.onload = function(e){
171 try{opt.aftersend()}catch(e){/*ignore*/}
172 if(200!==this.status){
173 opt.onerror(e);
174 return;
175 }
176 const orh = opt.responseHeaders;
@@ -176,9 +192,13 @@
192 opt.onload.apply(opt, args);
193 }catch(e){
194 opt.onerror(e);
195 }
196 };
197 try{opt.beforesend()}catch(e){/*ignore*/}
198 if(undefined!==payload) x.send(payload);
199 else x.send();
200 return this;
201 };
202
203 window.fossil.fetch.beforesend = function(){};
204 window.fossil.fetch.aftersend = function(){};
205
--- src/fossil.page.fileedit.js
+++ src/fossil.page.fileedit.js
@@ -461,11 +461,11 @@
461461
sel, btnClear);
462462
D.attr(wrapper, "title", [
463463
'Locally-edited files. Timestamps are the last local edit time.',
464464
'Only the',P.config.defaultMaxStashSize,'most recent checkin/file',
465465
'combinations are retained.',
466
- 'Committing or reloading a file removes it from this stash.'
466
+ 'Committing or reloading a file removes it from this list.'
467467
].join(' '));
468468
D.option(D.disable(sel), "(empty)");
469469
F.page.addEventListener('fileedit-stash-updated',(e)=>this.updateList(e.detail));
470470
F.page.addEventListener('fileedit-file-loaded',(e)=>this.updateList($stash, e.detail));
471471
sel.addEventListener('change',function(e){
@@ -558,10 +558,39 @@
558558
if(forceEvent){
559559
// Force UI update
560560
s.dispatchEvent(new Event('change',{target:s}));
561561
}
562562
};
563
+
564
+ /**
565
+ Keep track of how many in-flight AJAX requests there are so we
566
+ can disable input elements while any are pending. For
567
+ simplicity's sake we simply disable ALL OF IT while any AJAX is
568
+ pending, rather than disabling operation-specific UI elements,
569
+ which would be a huge maintenance hassle..
570
+ */
571
+ const ajaxState = {
572
+ count: 0 /* in-flight F.fetch() requests */,
573
+ toDisable: undefined /* elements to disable during ajax activity */
574
+ };
575
+ F.fetch.beforesend = function f(){
576
+ if(!ajaxState.toDisable){
577
+ ajaxState.toDisable = document.querySelectorAll(
578
+ 'button, input, select, textarea'
579
+ );
580
+ }
581
+ if(1===++ajaxState.count){
582
+ D.addClass(document.body, 'waiting');
583
+ D.disable(ajaxState.toDisable);
584
+ }
585
+ };
586
+ F.fetch.aftersend = function(){
587
+ if(0===--ajaxState.count){
588
+ D.removeClass(document.body, 'waiting');
589
+ D.enable(ajaxState.toDisable);
590
+ }
591
+ };
563592
564593
F.onPageLoad(function() {
565594
P.base = {tag: E('base')};
566595
P.base.originalHref = P.base.tag.href;
567596
P.tabs = new fossil.TabManager('#fileedit-tabs');
568597
--- src/fossil.page.fileedit.js
+++ src/fossil.page.fileedit.js
@@ -461,11 +461,11 @@
461 sel, btnClear);
462 D.attr(wrapper, "title", [
463 'Locally-edited files. Timestamps are the last local edit time.',
464 'Only the',P.config.defaultMaxStashSize,'most recent checkin/file',
465 'combinations are retained.',
466 'Committing or reloading a file removes it from this stash.'
467 ].join(' '));
468 D.option(D.disable(sel), "(empty)");
469 F.page.addEventListener('fileedit-stash-updated',(e)=>this.updateList(e.detail));
470 F.page.addEventListener('fileedit-file-loaded',(e)=>this.updateList($stash, e.detail));
471 sel.addEventListener('change',function(e){
@@ -558,10 +558,39 @@
558 if(forceEvent){
559 // Force UI update
560 s.dispatchEvent(new Event('change',{target:s}));
561 }
562 };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
563
564 F.onPageLoad(function() {
565 P.base = {tag: E('base')};
566 P.base.originalHref = P.base.tag.href;
567 P.tabs = new fossil.TabManager('#fileedit-tabs');
568
--- src/fossil.page.fileedit.js
+++ src/fossil.page.fileedit.js
@@ -461,11 +461,11 @@
461 sel, btnClear);
462 D.attr(wrapper, "title", [
463 'Locally-edited files. Timestamps are the last local edit time.',
464 'Only the',P.config.defaultMaxStashSize,'most recent checkin/file',
465 'combinations are retained.',
466 'Committing or reloading a file removes it from this list.'
467 ].join(' '));
468 D.option(D.disable(sel), "(empty)");
469 F.page.addEventListener('fileedit-stash-updated',(e)=>this.updateList(e.detail));
470 F.page.addEventListener('fileedit-file-loaded',(e)=>this.updateList($stash, e.detail));
471 sel.addEventListener('change',function(e){
@@ -558,10 +558,39 @@
558 if(forceEvent){
559 // Force UI update
560 s.dispatchEvent(new Event('change',{target:s}));
561 }
562 };
563
564 /**
565 Keep track of how many in-flight AJAX requests there are so we
566 can disable input elements while any are pending. For
567 simplicity's sake we simply disable ALL OF IT while any AJAX is
568 pending, rather than disabling operation-specific UI elements,
569 which would be a huge maintenance hassle..
570 */
571 const ajaxState = {
572 count: 0 /* in-flight F.fetch() requests */,
573 toDisable: undefined /* elements to disable during ajax activity */
574 };
575 F.fetch.beforesend = function f(){
576 if(!ajaxState.toDisable){
577 ajaxState.toDisable = document.querySelectorAll(
578 'button, input, select, textarea'
579 );
580 }
581 if(1===++ajaxState.count){
582 D.addClass(document.body, 'waiting');
583 D.disable(ajaxState.toDisable);
584 }
585 };
586 F.fetch.aftersend = function(){
587 if(0===--ajaxState.count){
588 D.removeClass(document.body, 'waiting');
589 D.enable(ajaxState.toDisable);
590 }
591 };
592
593 F.onPageLoad(function() {
594 P.base = {tag: E('base')};
595 P.base.originalHref = P.base.tag.href;
596 P.tabs = new fossil.TabManager('#fileedit-tabs');
597
--- src/style.fileedit.css
+++ src/style.fileedit.css
@@ -1,6 +1,10 @@
11
/** Styles specific to /fileedit... */
2
+body.fileedit.waiting * {
3
+ /* Triggered during AJAX requests. */
4
+ cursor: wait;
5
+}
26
body.fileedit .error {
37
padding: 0.25em;
48
}
59
body.fileedit .warning {
610
padding: 0.25em;
711
--- src/style.fileedit.css
+++ src/style.fileedit.css
@@ -1,6 +1,10 @@
1 /** Styles specific to /fileedit... */
 
 
 
 
2 body.fileedit .error {
3 padding: 0.25em;
4 }
5 body.fileedit .warning {
6 padding: 0.25em;
7
--- src/style.fileedit.css
+++ src/style.fileedit.css
@@ -1,6 +1,10 @@
1 /** Styles specific to /fileedit... */
2 body.fileedit.waiting * {
3 /* Triggered during AJAX requests. */
4 cursor: wait;
5 }
6 body.fileedit .error {
7 padding: 0.25em;
8 }
9 body.fileedit .warning {
10 padding: 0.25em;
11

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button