Fossil SCM

Removed the copy button's hard-coded post-copy behaviour (flashing) and instead fire a text-copied event. The line number selection now closes the popup widget after the copy button is triggered. Implemented a basic toast-message API using PopupWidget.

stephan 2020-08-16 00:50 line-number-selection
Commit 8a6ccf9ddd720f3cc5a5c4b2d0a0a954d6b3e49f5395ec91e2f63b79a2e3e051
--- src/default.css
+++ src/default.css
@@ -1250,5 +1250,16 @@
12501250
z-index: 100;
12511251
box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.75);
12521252
background-color: inherit;
12531253
font-size: 80%;
12541254
}
1255
+
1256
+.fossil-toast {/* "toast"-style popup message */
1257
+ padding: 0.25em 0.5em;
1258
+ margin: 0;
1259
+ border-radius: 0.25em;
1260
+ font-size: 1em;
1261
+ opacity: 0.8;
1262
+ border-size: 1px;
1263
+ border-style: dotted;
1264
+ border-color: rgb( 127, 127, 127, 0.5 );
1265
+}
12551266
--- src/default.css
+++ src/default.css
@@ -1250,5 +1250,16 @@
1250 z-index: 100;
1251 box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.75);
1252 background-color: inherit;
1253 font-size: 80%;
1254 }
 
 
 
 
 
 
 
 
 
 
 
1255
--- src/default.css
+++ src/default.css
@@ -1250,5 +1250,16 @@
1250 z-index: 100;
1251 box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.75);
1252 background-color: inherit;
1253 font-size: 80%;
1254 }
1255
1256 .fossil-toast {/* "toast"-style popup message */
1257 padding: 0.25em 0.5em;
1258 margin: 0;
1259 border-radius: 0.25em;
1260 font-size: 1em;
1261 opacity: 0.8;
1262 border-size: 1px;
1263 border-style: dotted;
1264 border-color: rgb( 127, 127, 127, 0.5 );
1265 }
1266
--- src/fossil.copybutton.js
+++ src/fossil.copybutton.js
@@ -4,14 +4,10 @@
44
55
Requires: fossil.bootstrap, fossil.dom
66
*/
77
const D = F.dom;
88
9
- const config = {
10
- blinkTimeMs: 400
11
- };
12
-
139
/**
1410
Initializes element e as a copy button using the given options
1511
object.
1612
1713
The first argument may be a DOM element or a string (CSS selector
@@ -40,10 +36,15 @@
4036
.cssClass: optional CSS class, or list of classes, to apply to e.
4137
4238
.style: optional object of properties to copy directly into
4339
e.style.
4440
41
+ .oncopy: an optional callback function which is added as an event
42
+ listener for the 'text-copied' event (see below). There is
43
+ functionally no difference from setting this option or adding a
44
+ 'text-copied' event listener to the element, and this option is
45
+ considered to be a convenience form of that.
4546
4647
Note that this function's own defaultOptions object holds default
4748
values for some options. Any changes made to that object affect
4849
any future calls to this function.
4950
@@ -50,14 +51,27 @@
5051
Be aware that clipboard functionality might or might not be
5152
available in any given environment. If this button appears to
5253
have no effect, that may be because it is not enabled/available
5354
in the current platform.
5455
56
+ The copy button emits custom event 'text-copied' after it has
57
+ successfully copied text to the clipboard. The event's "detail"
58
+ member is an object with a "text" property holding the copied
59
+ text. Other properties may be added in the future. The event is
60
+ not fired if copying to the clipboard fails (e.g. is not
61
+ available in the current environment).
62
+
63
+ Returns the copy-initialized element.
64
+
5565
Example:
5666
57
- fossil.copyButton('#my-copy-button', {
67
+ const button = fossil.copyButton('#my-copy-button', {
5868
copyFromId: 'some-other-element-id'
69
+ });
70
+ button.addEventListener('text-copied',function(ev){
71
+ fossil.dom.flashOnce(ev.target);
72
+ console.debug("Copied text:",ev.detail.text);
5973
});
6074
*/
6175
F.copyButton = function f(e, opt){
6276
if('string'===typeof e){
6377
e = document.querySelector(e);
@@ -78,20 +92,25 @@
7892
D.copyStyle(e, opt.style);
7993
e.addEventListener(
8094
'click',
8195
function(){
8296
const txt = extract.call(opt);
83
- //console.debug("extracted ",txt);
8497
if(txt && D.copyTextToClipboard(txt)){
85
- D.flashOnce(e, config.blinkTimeMs);
98
+ e.dispatchEvent(new CustomEvent('text-copied',{
99
+ detail: {text: txt}
100
+ }));
86101
}
87102
},
88103
false
89104
);
105
+ if('function' === typeof opt.oncopy){
106
+ e.addEventListener('text-copied', opt.oncopy, false);
107
+ }
108
+ return e;
90109
};
91110
92111
F.copyButton.defaultOptions = {
93112
cssClass: 'copy-button',
94113
style: {/*properties copied as-is into element.style*/}
95114
};
96115
97116
})(window.fossil);
98117
--- src/fossil.copybutton.js
+++ src/fossil.copybutton.js
@@ -4,14 +4,10 @@
4
5 Requires: fossil.bootstrap, fossil.dom
6 */
7 const D = F.dom;
8
9 const config = {
10 blinkTimeMs: 400
11 };
12
13 /**
14 Initializes element e as a copy button using the given options
15 object.
16
17 The first argument may be a DOM element or a string (CSS selector
@@ -40,10 +36,15 @@
40 .cssClass: optional CSS class, or list of classes, to apply to e.
41
42 .style: optional object of properties to copy directly into
43 e.style.
44
 
 
 
 
 
45
46 Note that this function's own defaultOptions object holds default
47 values for some options. Any changes made to that object affect
48 any future calls to this function.
49
@@ -50,14 +51,27 @@
50 Be aware that clipboard functionality might or might not be
51 available in any given environment. If this button appears to
52 have no effect, that may be because it is not enabled/available
53 in the current platform.
54
 
 
 
 
 
 
 
 
 
55 Example:
56
57 fossil.copyButton('#my-copy-button', {
58 copyFromId: 'some-other-element-id'
 
 
 
 
59 });
60 */
61 F.copyButton = function f(e, opt){
62 if('string'===typeof e){
63 e = document.querySelector(e);
@@ -78,20 +92,25 @@
78 D.copyStyle(e, opt.style);
79 e.addEventListener(
80 'click',
81 function(){
82 const txt = extract.call(opt);
83 //console.debug("extracted ",txt);
84 if(txt && D.copyTextToClipboard(txt)){
85 D.flashOnce(e, config.blinkTimeMs);
 
 
86 }
87 },
88 false
89 );
 
 
 
 
90 };
91
92 F.copyButton.defaultOptions = {
93 cssClass: 'copy-button',
94 style: {/*properties copied as-is into element.style*/}
95 };
96
97 })(window.fossil);
98
--- src/fossil.copybutton.js
+++ src/fossil.copybutton.js
@@ -4,14 +4,10 @@
4
5 Requires: fossil.bootstrap, fossil.dom
6 */
7 const D = F.dom;
8
 
 
 
 
9 /**
10 Initializes element e as a copy button using the given options
11 object.
12
13 The first argument may be a DOM element or a string (CSS selector
@@ -40,10 +36,15 @@
36 .cssClass: optional CSS class, or list of classes, to apply to e.
37
38 .style: optional object of properties to copy directly into
39 e.style.
40
41 .oncopy: an optional callback function which is added as an event
42 listener for the 'text-copied' event (see below). There is
43 functionally no difference from setting this option or adding a
44 'text-copied' event listener to the element, and this option is
45 considered to be a convenience form of that.
46
47 Note that this function's own defaultOptions object holds default
48 values for some options. Any changes made to that object affect
49 any future calls to this function.
50
@@ -50,14 +51,27 @@
51 Be aware that clipboard functionality might or might not be
52 available in any given environment. If this button appears to
53 have no effect, that may be because it is not enabled/available
54 in the current platform.
55
56 The copy button emits custom event 'text-copied' after it has
57 successfully copied text to the clipboard. The event's "detail"
58 member is an object with a "text" property holding the copied
59 text. Other properties may be added in the future. The event is
60 not fired if copying to the clipboard fails (e.g. is not
61 available in the current environment).
62
63 Returns the copy-initialized element.
64
65 Example:
66
67 const button = fossil.copyButton('#my-copy-button', {
68 copyFromId: 'some-other-element-id'
69 });
70 button.addEventListener('text-copied',function(ev){
71 fossil.dom.flashOnce(ev.target);
72 console.debug("Copied text:",ev.detail.text);
73 });
74 */
75 F.copyButton = function f(e, opt){
76 if('string'===typeof e){
77 e = document.querySelector(e);
@@ -78,20 +92,25 @@
92 D.copyStyle(e, opt.style);
93 e.addEventListener(
94 'click',
95 function(){
96 const txt = extract.call(opt);
 
97 if(txt && D.copyTextToClipboard(txt)){
98 e.dispatchEvent(new CustomEvent('text-copied',{
99 detail: {text: txt}
100 }));
101 }
102 },
103 false
104 );
105 if('function' === typeof opt.oncopy){
106 e.addEventListener('text-copied', opt.oncopy, false);
107 }
108 return e;
109 };
110
111 F.copyButton.defaultOptions = {
112 cssClass: 'copy-button',
113 style: {/*properties copied as-is into element.style*/}
114 };
115
116 })(window.fossil);
117
--- src/fossil.dom.js
+++ src/fossil.dom.js
@@ -480,17 +480,24 @@
480480
481481
This will only activate once per element during that timeframe -
482482
further calls will become no-ops until the blink is
483483
completed. This routine adds a dataset member to the element for
484484
the duration of the blink, to allow it to block multiple blinks.
485
+
486
+ If passed 2 arguments and the 2nd is a function, it behaves as if
487
+ it were called as (arg1, undefined, arg2).
485488
486489
Returns e, noting that the flash itself is asynchronous and may
487490
still be running, or not yet started, when this function returns.
488491
*/
489492
dom.flashOnce = function f(e,howLongMs,afterFlashCallback){
490493
if(e.dataset.isBlinking){
491494
return;
495
+ }
496
+ if(2===arguments.length && 'function' ===typeof howLongMs){
497
+ afterFlashCallback = howLongMs;
498
+ howLongMs = f.defaultTimeMs;
492499
}
493500
if(!howLongMs || 'number'!==typeof howLongMs){
494501
howLongMs = f.defaultTimeMs;
495502
}
496503
e.dataset.isBlinking = true;
497504
--- src/fossil.dom.js
+++ src/fossil.dom.js
@@ -480,17 +480,24 @@
480
481 This will only activate once per element during that timeframe -
482 further calls will become no-ops until the blink is
483 completed. This routine adds a dataset member to the element for
484 the duration of the blink, to allow it to block multiple blinks.
 
 
 
485
486 Returns e, noting that the flash itself is asynchronous and may
487 still be running, or not yet started, when this function returns.
488 */
489 dom.flashOnce = function f(e,howLongMs,afterFlashCallback){
490 if(e.dataset.isBlinking){
491 return;
 
 
 
 
492 }
493 if(!howLongMs || 'number'!==typeof howLongMs){
494 howLongMs = f.defaultTimeMs;
495 }
496 e.dataset.isBlinking = true;
497
--- src/fossil.dom.js
+++ src/fossil.dom.js
@@ -480,17 +480,24 @@
480
481 This will only activate once per element during that timeframe -
482 further calls will become no-ops until the blink is
483 completed. This routine adds a dataset member to the element for
484 the duration of the blink, to allow it to block multiple blinks.
485
486 If passed 2 arguments and the 2nd is a function, it behaves as if
487 it were called as (arg1, undefined, arg2).
488
489 Returns e, noting that the flash itself is asynchronous and may
490 still be running, or not yet started, when this function returns.
491 */
492 dom.flashOnce = function f(e,howLongMs,afterFlashCallback){
493 if(e.dataset.isBlinking){
494 return;
495 }
496 if(2===arguments.length && 'function' ===typeof howLongMs){
497 afterFlashCallback = howLongMs;
498 howLongMs = f.defaultTimeMs;
499 }
500 if(!howLongMs || 'number'!==typeof howLongMs){
501 howLongMs = f.defaultTimeMs;
502 }
503 e.dataset.isBlinking = true;
504
--- src/fossil.numbered-lines.js
+++ src/fossil.numbered-lines.js
@@ -49,12 +49,16 @@
4949
const btnCopy = D.span(),
5050
link = D.span();
5151
this.state = {link};
5252
F.copyButton(btnCopy,{
5353
copyFromElement: link,
54
- extractText: ()=>link.dataset.url
55
- });
54
+ extractText: ()=>link.dataset.url,
55
+ oncopy: (ev)=>{
56
+ F.toast("Copied: ",D.append(D.code(),ev.detail.text));
57
+ D.flashOnce(ev.target, undefined, ()=>lineTip.hide());
58
+ }
59
+ });//.addEventListener('text-copied', (ev)=>D.flashOnce(ev.target));
5660
D.append(this.e, btnCopy, link)
5761
}
5862
});
5963
6064
tbl.addEventListener('click', function f(ev){
6165
--- src/fossil.numbered-lines.js
+++ src/fossil.numbered-lines.js
@@ -49,12 +49,16 @@
49 const btnCopy = D.span(),
50 link = D.span();
51 this.state = {link};
52 F.copyButton(btnCopy,{
53 copyFromElement: link,
54 extractText: ()=>link.dataset.url
55 });
 
 
 
 
56 D.append(this.e, btnCopy, link)
57 }
58 });
59
60 tbl.addEventListener('click', function f(ev){
61
--- src/fossil.numbered-lines.js
+++ src/fossil.numbered-lines.js
@@ -49,12 +49,16 @@
49 const btnCopy = D.span(),
50 link = D.span();
51 this.state = {link};
52 F.copyButton(btnCopy,{
53 copyFromElement: link,
54 extractText: ()=>link.dataset.url,
55 oncopy: (ev)=>{
56 F.toast("Copied: ",D.append(D.code(),ev.detail.text));
57 D.flashOnce(ev.target, undefined, ()=>lineTip.hide());
58 }
59 });//.addEventListener('text-copied', (ev)=>D.flashOnce(ev.target));
60 D.append(this.e, btnCopy, link)
61 }
62 });
63
64 tbl.addEventListener('click', function f(ev){
65
--- src/fossil.popupwidget.js
+++ src/fossil.popupwidget.js
@@ -14,12 +14,13 @@
1414
Options:
1515
1616
.refresh: callback which is called just before the tooltip is
1717
revealed or moved. It must refresh the contents of the tooltip,
1818
if needed, by applying the content to/within this.e, which is the
19
- base DOM element for the tooltip. If the contents are static and
20
- set up via the .init option then this callback is not needed.
19
+ base DOM element for the tooltip (and is a child of
20
+ document.body). If the contents are static and set up via the
21
+ .init option then this callback is not needed.
2122
2223
.adjustX: an optional callback which is called when the tooltip
2324
is to be displayed at a given position and passed the X
2425
viewport-relative coordinate. This routine must either return its
2526
argument as-is or return an adjusted value. The intent is to
@@ -144,11 +145,11 @@
144145
Sidebar: showing/hiding the widget is, as is conventional for
145146
this framework, done by removing/adding the 'hidden' CSS class
146147
to it, so that class must be defined appropriately.
147148
*/
148149
show: function(){
149
- var x = 0, y = 0, showIt;
150
+ var x = undefined, y = undefined, showIt;
150151
if(2===arguments.length){
151152
x = arguments[0];
152153
y = arguments[1];
153154
showIt = true;
154155
}else if(1===arguments.length){
@@ -167,15 +168,63 @@
167168
x = this.options.adjustX.call(this,x);
168169
y = this.options.adjustY.call(this,y);
169170
x += window.pageXOffset;
170171
y += window.pageYOffset;
171172
}
172
- D[showIt ? 'removeClass' : 'addClass'](this.e, 'hidden');
173
- if(x || y){
174
- this.e.style.left = x+"px";
175
- this.e.style.top = y+"px";
173
+ console.debug("showIt?",showIt,x,y);
174
+ if(showIt){
175
+ if('number'===typeof x && 'number'===typeof y){
176
+ this.e.style.left = x+"px";
177
+ this.e.style.top = y+"px";
178
+ }
179
+ D.removeClass(this.e, 'hidden');
180
+ }else{
181
+ D.addClass(this.e, 'hidden');
182
+ delete this.e.style.removeProperty('left');
183
+ delete this.e.style.removeProperty('top');
176184
}
177185
return this;
178
- }
186
+ },
187
+
188
+ hide: function(){return this.show(false)}
179189
}/*F.PopupWidget.prototype*/;
180
-
190
+
191
+ /**
192
+ Convenience wrapper around a PopupWidget which pops up a shared
193
+ PopupWidget instance to show toast-style messages (commonly seen
194
+ on Android). Its arguments may be anything suitable for passing
195
+ to fossil.dom.append(), and each argument is first append()ed to
196
+ the toast widget, then the widget is shown for
197
+ F.toast.config.displayTimeMs milliseconds. This is called while
198
+ a toast is currently being displayed, the first will be overwritten
199
+ and the time until the message is hidden will be reset.
200
+
201
+ The toast is always shown at the viewport-relative coordinates
202
+ defined by the F.toast.config.position.
203
+
204
+ The toaster's DOM element has the CSS classes fossil-tooltip
205
+ and fossil-toast, so can be style via those.
206
+ */
207
+ F.toast = function f(/*...*/){
208
+ if(!f.toast){
209
+ f.toast = function ff(argsObject){
210
+ if(!ff.toaster) ff.toaster = new F.PopupWidget({
211
+ cssClass: ['fossil-tooltip', 'fossil-toast']
212
+ });
213
+ if(f._timer) clearTimeout(f._timer);
214
+ D.clearElement(ff.toaster.e);
215
+ var i = 0;
216
+ for( ; i < argsObject.length; ++i ){
217
+ D.append(ff.toaster.e, argsObject[i]);
218
+ };
219
+ ff.toaster.show(f.config.position.x, f.config.position.y);
220
+ f._timer = setTimeout(()=>ff.toaster.hide(), f.config.displayTimeMs);
221
+ };
222
+ }
223
+ f.toast(arguments);
224
+ };
225
+ F.toast.config = {
226
+ position: { x: 5, y: 5 /*viewport-relative, pixels*/ },
227
+ displayTimeMs: 2500
228
+ };
229
+
181230
})(window.fossil);
182231
--- src/fossil.popupwidget.js
+++ src/fossil.popupwidget.js
@@ -14,12 +14,13 @@
14 Options:
15
16 .refresh: callback which is called just before the tooltip is
17 revealed or moved. It must refresh the contents of the tooltip,
18 if needed, by applying the content to/within this.e, which is the
19 base DOM element for the tooltip. If the contents are static and
20 set up via the .init option then this callback is not needed.
 
21
22 .adjustX: an optional callback which is called when the tooltip
23 is to be displayed at a given position and passed the X
24 viewport-relative coordinate. This routine must either return its
25 argument as-is or return an adjusted value. The intent is to
@@ -144,11 +145,11 @@
144 Sidebar: showing/hiding the widget is, as is conventional for
145 this framework, done by removing/adding the 'hidden' CSS class
146 to it, so that class must be defined appropriately.
147 */
148 show: function(){
149 var x = 0, y = 0, showIt;
150 if(2===arguments.length){
151 x = arguments[0];
152 y = arguments[1];
153 showIt = true;
154 }else if(1===arguments.length){
@@ -167,15 +168,63 @@
167 x = this.options.adjustX.call(this,x);
168 y = this.options.adjustY.call(this,y);
169 x += window.pageXOffset;
170 y += window.pageYOffset;
171 }
172 D[showIt ? 'removeClass' : 'addClass'](this.e, 'hidden');
173 if(x || y){
174 this.e.style.left = x+"px";
175 this.e.style.top = y+"px";
 
 
 
 
 
 
 
176 }
177 return this;
178 }
 
 
179 }/*F.PopupWidget.prototype*/;
180
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181 })(window.fossil);
182
--- src/fossil.popupwidget.js
+++ src/fossil.popupwidget.js
@@ -14,12 +14,13 @@
14 Options:
15
16 .refresh: callback which is called just before the tooltip is
17 revealed or moved. It must refresh the contents of the tooltip,
18 if needed, by applying the content to/within this.e, which is the
19 base DOM element for the tooltip (and is a child of
20 document.body). If the contents are static and set up via the
21 .init option then this callback is not needed.
22
23 .adjustX: an optional callback which is called when the tooltip
24 is to be displayed at a given position and passed the X
25 viewport-relative coordinate. This routine must either return its
26 argument as-is or return an adjusted value. The intent is to
@@ -144,11 +145,11 @@
145 Sidebar: showing/hiding the widget is, as is conventional for
146 this framework, done by removing/adding the 'hidden' CSS class
147 to it, so that class must be defined appropriately.
148 */
149 show: function(){
150 var x = undefined, y = undefined, showIt;
151 if(2===arguments.length){
152 x = arguments[0];
153 y = arguments[1];
154 showIt = true;
155 }else if(1===arguments.length){
@@ -167,15 +168,63 @@
168 x = this.options.adjustX.call(this,x);
169 y = this.options.adjustY.call(this,y);
170 x += window.pageXOffset;
171 y += window.pageYOffset;
172 }
173 console.debug("showIt?",showIt,x,y);
174 if(showIt){
175 if('number'===typeof x && 'number'===typeof y){
176 this.e.style.left = x+"px";
177 this.e.style.top = y+"px";
178 }
179 D.removeClass(this.e, 'hidden');
180 }else{
181 D.addClass(this.e, 'hidden');
182 delete this.e.style.removeProperty('left');
183 delete this.e.style.removeProperty('top');
184 }
185 return this;
186 },
187
188 hide: function(){return this.show(false)}
189 }/*F.PopupWidget.prototype*/;
190
191 /**
192 Convenience wrapper around a PopupWidget which pops up a shared
193 PopupWidget instance to show toast-style messages (commonly seen
194 on Android). Its arguments may be anything suitable for passing
195 to fossil.dom.append(), and each argument is first append()ed to
196 the toast widget, then the widget is shown for
197 F.toast.config.displayTimeMs milliseconds. This is called while
198 a toast is currently being displayed, the first will be overwritten
199 and the time until the message is hidden will be reset.
200
201 The toast is always shown at the viewport-relative coordinates
202 defined by the F.toast.config.position.
203
204 The toaster's DOM element has the CSS classes fossil-tooltip
205 and fossil-toast, so can be style via those.
206 */
207 F.toast = function f(/*...*/){
208 if(!f.toast){
209 f.toast = function ff(argsObject){
210 if(!ff.toaster) ff.toaster = new F.PopupWidget({
211 cssClass: ['fossil-tooltip', 'fossil-toast']
212 });
213 if(f._timer) clearTimeout(f._timer);
214 D.clearElement(ff.toaster.e);
215 var i = 0;
216 for( ; i < argsObject.length; ++i ){
217 D.append(ff.toaster.e, argsObject[i]);
218 };
219 ff.toaster.show(f.config.position.x, f.config.position.y);
220 f._timer = setTimeout(()=>ff.toaster.hide(), f.config.displayTimeMs);
221 };
222 }
223 f.toast(arguments);
224 };
225 F.toast.config = {
226 position: { x: 5, y: 5 /*viewport-relative, pixels*/ },
227 displayTimeMs: 2500
228 };
229
230 })(window.fossil);
231

Keyboard Shortcuts

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