Fossil SCM

Added a 'tick' mode to fossil.confirmer to more easily allow the triggering element to be visibly updated to reflect the countdown state. The editor's discard/reload button now visibly counts down from 3 if clicked.

stephan 2020-05-06 23:53 fileedit-ajaxify
Commit 3da4b94c44411de4632d0eb2dcfbff29c0d5e7d6ead85d3e134eece5ab898082
--- src/fossil.confirmer.js
+++ src/fossil.confirmer.js
@@ -23,19 +23,22 @@
2323
2424
Options:
2525
2626
.initialText = initial text of the element. Defaults to the result
2727
of the element's .value (for INPUT tags) or innerHTML (for
28
- everything else).
28
+ everything else). After the timeout/tick count expires, or if the
29
+ user confirms the operation, the element's text is re-set to this
30
+ value.
2931
3032
.confirmText = text to show when in "confirm mode".
3133
Default=("Confirm: "+initialText), or something similar.
3234
3335
.timeout = Number of milliseconds to wait for confirmation.
34
- Default=3000.
36
+ Default=3000. Alternately, use a combination of .ticks and
37
+ .ticktime.
3538
36
- .onconfirm = function to call when clicked in confirm mode. Default
39
+ .onconfirm = function to call when clicked in confirm mode. Default
3740
= undefined. The function's "this" is the the DOM element to which
3841
the countdown applies.
3942
4043
.ontimeout = function to call when confirm is not issued. Default =
4144
undefined. The function's "this" is the DOM element to which the
@@ -57,24 +60,60 @@
5760
.classWaiting = optional CSS class string (default='') which is
5861
added to the target when it is waiting on a timeout. When the target
5962
leaves timeout-wait mode, this class is removed. When timeout-wait
6063
mode is entered, this class is added *before* the .onactivate
6164
handler is called.
65
+
66
+ .ticktime = a number of ms to wait per tick (see the next item).
67
+ Default = 1000.
68
+
69
+ .ticks = a number of "ticks" to wait, as an alternative to .timeout.
70
+ When this mode is active, the ontick callback will be triggered
71
+ immediately before each tick, including the first one. If both
72
+ .ticks and .timeout are set, only one will be used, but which one is
73
+ unspecified. If passed a ticks value with a truncated integer value
74
+ of 0 or less, it will throw an exception (e.g. that also applies if
75
+ it's passed 0.5).
76
+
77
+ .ontick = when using .ticks, this callback is passed the current
78
+ tick number before each tick, and its "this" is the target
79
+ element. On each subsequent call, the tick count will be reduced by
80
+ 1, and it is passed 0 after the final tick expires or when the
81
+ action has been confirmed, immediately before the onconfirm or
82
+ ontimeout callback. The intention of the callback is to update the
83
+ label of the target element. If .ticks is set but .ontick is not
84
+ then a default implementation is used which updates the element with
85
+ the .confirmText, prepending a countdown to it.
6286
6387
.debug = boolean. If truthy, it sends some debug output to the dev
6488
console to track what it's doing.
6589
66
-Due to the nature of multi-threaded code, it is potentially possible
67
-that confirmation and timeout actions BOTH happen if the user triggers
68
-the associated action at "just the right millisecond" before the
69
-timeout is triggered.
90
+Various notes:
91
+
92
+- To change the default option values, modify the
93
+ fossil.confirmer.defaultOpts object.
94
+
95
+- Exceptions triggered via the callbacks are caught and emitted to the
96
+ dev console if the debug option is enabled, but are otherwise
97
+ ignored.
98
+
99
+- Due to the nature of multi-threaded code, it is potentially possible
100
+ that confirmation and timeout actions BOTH happen if the user
101
+ triggers the associated action at "just the right millisecond"
102
+ before the timeout is triggered.
70103
71
-To change the default option values, modify the
72
-fossil.confirmer.defaultOpts object.
104
+TODO: add an invert option which activates if the timeout is reached
105
+and "times out" if the element is clicked again. e.g. a button which
106
+says "Saving..." and cancels the op if it's clicked again, else it
107
+saves after X time/ticks.
73108
74109
Terse Change history:
75110
111
+- 20200507:
112
+ - Add a tick-based countdown in order to more easily support
113
+ updating the target element with the countdown.
114
+
76115
- 20200506:
77116
- Ported from jQuery to plain JS.
78117
79118
- 20181112:
80119
- extended to support certain INPUT elements.
@@ -90,56 +129,95 @@
90129
dbg("confirmer opt =",opt);
91130
if(!f.Holder){
92131
f.isInput = (e)=>/^(input|textarea)$/i.test(e.nodeName);
93132
f.Holder = function(target,opt){
94133
const self = this;
95
- self.target = target;
96
- self.opt = opt;
97
- self.timerID = undefined;
98
- self.state = this.states.initial;
134
+ this.target = target;
135
+ this.opt = opt;
136
+ this.timerID = undefined;
137
+ this.state = this.states.initial;
99138
const isInput = f.isInput(target);
100139
const updateText = function(msg){
101140
if(isInput) target.value = msg;
102141
else target.innerHTML = msg;
103142
}
104
- updateText(self.opt.initialText);
143
+ updateText(this.opt.initialText);
144
+ if(this.opt.ticks && !this.opt.ontick){
145
+ this.opt.ontick = function(tick){
146
+ updateText("("+tick+") "+self.opt.confirmText);
147
+ };
148
+ }
105149
this.setClasses(false);
106150
this.doTimeout = function() {
107
- this.timerID = undefined;
151
+ if(this.timerID){
152
+ clearTimeout( this.timerID );
153
+ delete this.timerID;
154
+ }
108155
if( this.state != this.states.waiting ) {
109156
// it was already confirmed
110157
return;
111158
}
112159
this.setClasses( false );
113160
this.state = this.states.initial;
114161
dbg("Timeout triggered.");
115
- updateText(this.opt.initialText);
162
+ if( this.opt.ontick ){
163
+ try{this.opt.ontick.call(this.target, 0)}
164
+ catch(e){dbg("ontick EXCEPTION:",e)}
165
+ }
116166
if( this.opt.ontimeout ) {
117
- this.opt.ontimeout.call(this.target);
167
+ try{this.opt.ontimeout.call(this.target)}
168
+ catch(e){dbg("ontimeout EXCEPTION:",e)}
118169
}
170
+ updateText(this.opt.initialText);
119171
};
120172
target.addEventListener(
121173
'click', function(){
122174
switch( self.state ) {
123175
case( self.states.waiting ):
176
+ /* Cancel the wait on confirmation */
124177
if( undefined !== self.timerID ){
125178
clearTimeout( self.timerID );
126179
delete self.timerID;
127180
}
128181
self.state = self.states.initial;
129182
self.setClasses( false );
130183
dbg("Confirmed");
184
+ if( self.opt.ontick ){
185
+ try{self.opt.ontick.call(self.target,0)}
186
+ catch(e){dbg("ontick EXCEPTION:",e)}
187
+ }
188
+ if( self.opt.onconfirm ){
189
+ try{self.opt.onconfirm.call(self.target)}
190
+ catch(e){dbg("onconfirm EXCEPTION:",e)}
191
+ }
131192
updateText(self.opt.initialText);
132
- if( self.opt.onconfirm ) self.opt.onconfirm.call(self.target);
133193
break;
134194
case( self.states.initial ):
195
+ /* Enter the waiting-on-confirmation state... */
196
+ if(self.opt.ticks) self.opt.currentTick = self.opt.ticks;
135197
self.setClasses( true );
136
- if( self.opt.onactivate ) self.opt.onactivate.call( self.target );
137198
self.state = self.states.waiting;
138
- dbg("Waiting "+self.opt.timeout+"ms on confirmation...");
139199
updateText( self.opt.confirmText );
140
- self.timerID = setTimeout(function(){self.doTimeout();},self.opt.timeout );
200
+ if( self.opt.onactivate ) self.opt.onactivate.call( self.target );
201
+ if( self.opt.ontick ) self.opt.ontick.call(self.target, self.opt.currentTick);
202
+ if(self.opt.timeout){
203
+ dbg("Waiting "+self.opt.timeout+"ms on confirmation...");
204
+ self.timerID =
205
+ setTimeout(()=>self.doTimeout(),self.opt.timeout );
206
+ }else if(self.opt.ticks){
207
+ dbg("Waiting on confirmation for "+self.opt.ticks
208
+ +" ticks of "+self.opt.ticktime+"ms each...");
209
+ self.timerID =
210
+ setInterval(function(){
211
+ if(0===--self.opt.currentTick) self.doTimeout();
212
+ else{
213
+ try{self.opt.ontick.call(self.target,
214
+ self.opt.currentTick)}
215
+ catch(e){dbg("ontick EXCEPTION:",e)}
216
+ }
217
+ },self.opt.ticktime);
218
+ }
141219
break;
142220
default: // can't happen.
143221
break;
144222
}
145223
}, false
@@ -172,10 +250,21 @@
172250
) || "PLEASE SET .initialText"
173251
},opt);
174252
if(!opt.confirmText){
175253
opt.confirmText = "Confirm: "+opt.initialText;
176254
}
255
+ if(opt.ticks){
256
+ delete opt.timeout;
257
+ opt.ticks = 0 | opt.ticks /* ensure it's an integer */;
258
+ if(opt.ticks<=0){
259
+ throw new Error("ticks must be >0");
260
+ }
261
+ if(opt.ticktime <= 0) opt.ticktime = 1000;
262
+ }else{
263
+ delete opt.ontick;
264
+ delete opt.ticks;
265
+ }
177266
new f.Holder(elem,opt);
178267
return this;
179268
};
180269
/**
181270
The default options for initConfirmer(). Tweak them to set the
@@ -183,14 +272,16 @@
183272
dynamically-generated, and can't reasonably be set in the
184273
defaults.
185274
*/
186275
F.confirmer.defaultOpts = {
187276
timeout:3000,
277
+ ticks: undefined,
278
+ ticktime: 998/*not *quite* 1000*/,
188279
onconfirm: undefined,
189280
ontimeout: undefined,
190281
onactivate: undefined,
191282
classInitial: '',
192283
classWaiting: '',
193284
debug: false
194285
};
195286
196287
})(window.fossil);
197288
--- src/fossil.confirmer.js
+++ src/fossil.confirmer.js
@@ -23,19 +23,22 @@
23
24 Options:
25
26 .initialText = initial text of the element. Defaults to the result
27 of the element's .value (for INPUT tags) or innerHTML (for
28 everything else).
 
 
29
30 .confirmText = text to show when in "confirm mode".
31 Default=("Confirm: "+initialText), or something similar.
32
33 .timeout = Number of milliseconds to wait for confirmation.
34 Default=3000.
 
35
36 .onconfirm = function to call when clicked in confirm mode. Default
37 = undefined. The function's "this" is the the DOM element to which
38 the countdown applies.
39
40 .ontimeout = function to call when confirm is not issued. Default =
41 undefined. The function's "this" is the DOM element to which the
@@ -57,24 +60,60 @@
57 .classWaiting = optional CSS class string (default='') which is
58 added to the target when it is waiting on a timeout. When the target
59 leaves timeout-wait mode, this class is removed. When timeout-wait
60 mode is entered, this class is added *before* the .onactivate
61 handler is called.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
63 .debug = boolean. If truthy, it sends some debug output to the dev
64 console to track what it's doing.
65
66 Due to the nature of multi-threaded code, it is potentially possible
67 that confirmation and timeout actions BOTH happen if the user triggers
68 the associated action at "just the right millisecond" before the
69 timeout is triggered.
 
 
 
 
 
 
 
 
 
70
71 To change the default option values, modify the
72 fossil.confirmer.defaultOpts object.
 
 
73
74 Terse Change history:
75
 
 
 
 
76 - 20200506:
77 - Ported from jQuery to plain JS.
78
79 - 20181112:
80 - extended to support certain INPUT elements.
@@ -90,56 +129,95 @@
90 dbg("confirmer opt =",opt);
91 if(!f.Holder){
92 f.isInput = (e)=>/^(input|textarea)$/i.test(e.nodeName);
93 f.Holder = function(target,opt){
94 const self = this;
95 self.target = target;
96 self.opt = opt;
97 self.timerID = undefined;
98 self.state = this.states.initial;
99 const isInput = f.isInput(target);
100 const updateText = function(msg){
101 if(isInput) target.value = msg;
102 else target.innerHTML = msg;
103 }
104 updateText(self.opt.initialText);
 
 
 
 
 
105 this.setClasses(false);
106 this.doTimeout = function() {
107 this.timerID = undefined;
 
 
 
108 if( this.state != this.states.waiting ) {
109 // it was already confirmed
110 return;
111 }
112 this.setClasses( false );
113 this.state = this.states.initial;
114 dbg("Timeout triggered.");
115 updateText(this.opt.initialText);
 
 
 
116 if( this.opt.ontimeout ) {
117 this.opt.ontimeout.call(this.target);
 
118 }
 
119 };
120 target.addEventListener(
121 'click', function(){
122 switch( self.state ) {
123 case( self.states.waiting ):
 
124 if( undefined !== self.timerID ){
125 clearTimeout( self.timerID );
126 delete self.timerID;
127 }
128 self.state = self.states.initial;
129 self.setClasses( false );
130 dbg("Confirmed");
 
 
 
 
 
 
 
 
131 updateText(self.opt.initialText);
132 if( self.opt.onconfirm ) self.opt.onconfirm.call(self.target);
133 break;
134 case( self.states.initial ):
 
 
135 self.setClasses( true );
136 if( self.opt.onactivate ) self.opt.onactivate.call( self.target );
137 self.state = self.states.waiting;
138 dbg("Waiting "+self.opt.timeout+"ms on confirmation...");
139 updateText( self.opt.confirmText );
140 self.timerID = setTimeout(function(){self.doTimeout();},self.opt.timeout );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141 break;
142 default: // can't happen.
143 break;
144 }
145 }, false
@@ -172,10 +250,21 @@
172 ) || "PLEASE SET .initialText"
173 },opt);
174 if(!opt.confirmText){
175 opt.confirmText = "Confirm: "+opt.initialText;
176 }
 
 
 
 
 
 
 
 
 
 
 
177 new f.Holder(elem,opt);
178 return this;
179 };
180 /**
181 The default options for initConfirmer(). Tweak them to set the
@@ -183,14 +272,16 @@
183 dynamically-generated, and can't reasonably be set in the
184 defaults.
185 */
186 F.confirmer.defaultOpts = {
187 timeout:3000,
 
 
188 onconfirm: undefined,
189 ontimeout: undefined,
190 onactivate: undefined,
191 classInitial: '',
192 classWaiting: '',
193 debug: false
194 };
195
196 })(window.fossil);
197
--- src/fossil.confirmer.js
+++ src/fossil.confirmer.js
@@ -23,19 +23,22 @@
23
24 Options:
25
26 .initialText = initial text of the element. Defaults to the result
27 of the element's .value (for INPUT tags) or innerHTML (for
28 everything else). After the timeout/tick count expires, or if the
29 user confirms the operation, the element's text is re-set to this
30 value.
31
32 .confirmText = text to show when in "confirm mode".
33 Default=("Confirm: "+initialText), or something similar.
34
35 .timeout = Number of milliseconds to wait for confirmation.
36 Default=3000. Alternately, use a combination of .ticks and
37 .ticktime.
38
39 .onconfirm = function to call when clicked in confirm mode. Default
40 = undefined. The function's "this" is the the DOM element to which
41 the countdown applies.
42
43 .ontimeout = function to call when confirm is not issued. Default =
44 undefined. The function's "this" is the DOM element to which the
@@ -57,24 +60,60 @@
60 .classWaiting = optional CSS class string (default='') which is
61 added to the target when it is waiting on a timeout. When the target
62 leaves timeout-wait mode, this class is removed. When timeout-wait
63 mode is entered, this class is added *before* the .onactivate
64 handler is called.
65
66 .ticktime = a number of ms to wait per tick (see the next item).
67 Default = 1000.
68
69 .ticks = a number of "ticks" to wait, as an alternative to .timeout.
70 When this mode is active, the ontick callback will be triggered
71 immediately before each tick, including the first one. If both
72 .ticks and .timeout are set, only one will be used, but which one is
73 unspecified. If passed a ticks value with a truncated integer value
74 of 0 or less, it will throw an exception (e.g. that also applies if
75 it's passed 0.5).
76
77 .ontick = when using .ticks, this callback is passed the current
78 tick number before each tick, and its "this" is the target
79 element. On each subsequent call, the tick count will be reduced by
80 1, and it is passed 0 after the final tick expires or when the
81 action has been confirmed, immediately before the onconfirm or
82 ontimeout callback. The intention of the callback is to update the
83 label of the target element. If .ticks is set but .ontick is not
84 then a default implementation is used which updates the element with
85 the .confirmText, prepending a countdown to it.
86
87 .debug = boolean. If truthy, it sends some debug output to the dev
88 console to track what it's doing.
89
90 Various notes:
91
92 - To change the default option values, modify the
93 fossil.confirmer.defaultOpts object.
94
95 - Exceptions triggered via the callbacks are caught and emitted to the
96 dev console if the debug option is enabled, but are otherwise
97 ignored.
98
99 - Due to the nature of multi-threaded code, it is potentially possible
100 that confirmation and timeout actions BOTH happen if the user
101 triggers the associated action at "just the right millisecond"
102 before the timeout is triggered.
103
104 TODO: add an invert option which activates if the timeout is reached
105 and "times out" if the element is clicked again. e.g. a button which
106 says "Saving..." and cancels the op if it's clicked again, else it
107 saves after X time/ticks.
108
109 Terse Change history:
110
111 - 20200507:
112 - Add a tick-based countdown in order to more easily support
113 updating the target element with the countdown.
114
115 - 20200506:
116 - Ported from jQuery to plain JS.
117
118 - 20181112:
119 - extended to support certain INPUT elements.
@@ -90,56 +129,95 @@
129 dbg("confirmer opt =",opt);
130 if(!f.Holder){
131 f.isInput = (e)=>/^(input|textarea)$/i.test(e.nodeName);
132 f.Holder = function(target,opt){
133 const self = this;
134 this.target = target;
135 this.opt = opt;
136 this.timerID = undefined;
137 this.state = this.states.initial;
138 const isInput = f.isInput(target);
139 const updateText = function(msg){
140 if(isInput) target.value = msg;
141 else target.innerHTML = msg;
142 }
143 updateText(this.opt.initialText);
144 if(this.opt.ticks && !this.opt.ontick){
145 this.opt.ontick = function(tick){
146 updateText("("+tick+") "+self.opt.confirmText);
147 };
148 }
149 this.setClasses(false);
150 this.doTimeout = function() {
151 if(this.timerID){
152 clearTimeout( this.timerID );
153 delete this.timerID;
154 }
155 if( this.state != this.states.waiting ) {
156 // it was already confirmed
157 return;
158 }
159 this.setClasses( false );
160 this.state = this.states.initial;
161 dbg("Timeout triggered.");
162 if( this.opt.ontick ){
163 try{this.opt.ontick.call(this.target, 0)}
164 catch(e){dbg("ontick EXCEPTION:",e)}
165 }
166 if( this.opt.ontimeout ) {
167 try{this.opt.ontimeout.call(this.target)}
168 catch(e){dbg("ontimeout EXCEPTION:",e)}
169 }
170 updateText(this.opt.initialText);
171 };
172 target.addEventListener(
173 'click', function(){
174 switch( self.state ) {
175 case( self.states.waiting ):
176 /* Cancel the wait on confirmation */
177 if( undefined !== self.timerID ){
178 clearTimeout( self.timerID );
179 delete self.timerID;
180 }
181 self.state = self.states.initial;
182 self.setClasses( false );
183 dbg("Confirmed");
184 if( self.opt.ontick ){
185 try{self.opt.ontick.call(self.target,0)}
186 catch(e){dbg("ontick EXCEPTION:",e)}
187 }
188 if( self.opt.onconfirm ){
189 try{self.opt.onconfirm.call(self.target)}
190 catch(e){dbg("onconfirm EXCEPTION:",e)}
191 }
192 updateText(self.opt.initialText);
 
193 break;
194 case( self.states.initial ):
195 /* Enter the waiting-on-confirmation state... */
196 if(self.opt.ticks) self.opt.currentTick = self.opt.ticks;
197 self.setClasses( true );
 
198 self.state = self.states.waiting;
 
199 updateText( self.opt.confirmText );
200 if( self.opt.onactivate ) self.opt.onactivate.call( self.target );
201 if( self.opt.ontick ) self.opt.ontick.call(self.target, self.opt.currentTick);
202 if(self.opt.timeout){
203 dbg("Waiting "+self.opt.timeout+"ms on confirmation...");
204 self.timerID =
205 setTimeout(()=>self.doTimeout(),self.opt.timeout );
206 }else if(self.opt.ticks){
207 dbg("Waiting on confirmation for "+self.opt.ticks
208 +" ticks of "+self.opt.ticktime+"ms each...");
209 self.timerID =
210 setInterval(function(){
211 if(0===--self.opt.currentTick) self.doTimeout();
212 else{
213 try{self.opt.ontick.call(self.target,
214 self.opt.currentTick)}
215 catch(e){dbg("ontick EXCEPTION:",e)}
216 }
217 },self.opt.ticktime);
218 }
219 break;
220 default: // can't happen.
221 break;
222 }
223 }, false
@@ -172,10 +250,21 @@
250 ) || "PLEASE SET .initialText"
251 },opt);
252 if(!opt.confirmText){
253 opt.confirmText = "Confirm: "+opt.initialText;
254 }
255 if(opt.ticks){
256 delete opt.timeout;
257 opt.ticks = 0 | opt.ticks /* ensure it's an integer */;
258 if(opt.ticks<=0){
259 throw new Error("ticks must be >0");
260 }
261 if(opt.ticktime <= 0) opt.ticktime = 1000;
262 }else{
263 delete opt.ontick;
264 delete opt.ticks;
265 }
266 new f.Holder(elem,opt);
267 return this;
268 };
269 /**
270 The default options for initConfirmer(). Tweak them to set the
@@ -183,14 +272,16 @@
272 dynamically-generated, and can't reasonably be set in the
273 defaults.
274 */
275 F.confirmer.defaultOpts = {
276 timeout:3000,
277 ticks: undefined,
278 ticktime: 998/*not *quite* 1000*/,
279 onconfirm: undefined,
280 ontimeout: undefined,
281 onactivate: undefined,
282 classInitial: '',
283 classWaiting: '',
284 debug: false
285 };
286
287 })(window.fossil);
288
--- src/fossil.page.fileedit.js
+++ src/fossil.page.fileedit.js
@@ -54,13 +54,15 @@
5454
);
5555
P.e.btnCommit.addEventListener(
5656
"click",(e)=>P.commit(), false
5757
);
5858
if(P.e.btnReload){
59
+ const label = "Really reload, losing edits?";
5960
F.confirmer(P.e.btnReload, {
60
- confirmText: "Really reload, losing edits?",
61
- onconfirm: (e)=>P.loadFile()
61
+ confirmText: label,
62
+ onconfirm: (e)=>P.loadFile(),
63
+ ticks: 3
6264
});
6365
}
6466
/**
6567
Cosmetic: jump through some hoops to enable/disable
6668
certain preview options depending on the current
6769
--- src/fossil.page.fileedit.js
+++ src/fossil.page.fileedit.js
@@ -54,13 +54,15 @@
54 );
55 P.e.btnCommit.addEventListener(
56 "click",(e)=>P.commit(), false
57 );
58 if(P.e.btnReload){
 
59 F.confirmer(P.e.btnReload, {
60 confirmText: "Really reload, losing edits?",
61 onconfirm: (e)=>P.loadFile()
 
62 });
63 }
64 /**
65 Cosmetic: jump through some hoops to enable/disable
66 certain preview options depending on the current
67
--- src/fossil.page.fileedit.js
+++ src/fossil.page.fileedit.js
@@ -54,13 +54,15 @@
54 );
55 P.e.btnCommit.addEventListener(
56 "click",(e)=>P.commit(), false
57 );
58 if(P.e.btnReload){
59 const label = "Really reload, losing edits?";
60 F.confirmer(P.e.btnReload, {
61 confirmText: label,
62 onconfirm: (e)=>P.loadFile(),
63 ticks: 3
64 });
65 }
66 /**
67 Cosmetic: jump through some hoops to enable/disable
68 certain preview options depending on the current
69

Keyboard Shortcuts

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