Fossil

Check-in [8a6ccf9ddd]
Login

Check-in [8a6ccf9ddd]

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment: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.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | line-number-selection
Files: files | file ages | folders
SHA3-256: 8a6ccf9ddd720f3cc5a5c4b2d0a0a954d6b3e49f5395ec91e2f63b79a2e3e051
User & Date: stephan 2020-08-16 00:50:31.984
Context
2020-08-16
01:32
Removed some stray debug output. Added a window.CustomEvent polyfill "just in case." ... (check-in: af47d1531d user: stephan tags: line-number-selection)
00:50
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. ... (check-in: 8a6ccf9ddd user: stephan tags: line-number-selection)
2020-08-15
23:30
Renamed TooltipWidget to PopupWidget because's it's not *quite* a tooltip and we're soon going to need something closer to a genuine tooltip. Minor adjacent cleanups and code consolidation. ... (check-in: 3998ccef44 user: stephan tags: line-number-selection)
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/default.css.
1248
1249
1250
1251
1252
1253
1254











  position: absolute;
  display: inline-block;
  z-index: 100;
  box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.75);
  background-color: inherit;
  font-size: 80%;
}


















>
>
>
>
>
>
>
>
>
>
>
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
  position: absolute;
  display: inline-block;
  z-index: 100;
  box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.75);
  background-color: inherit;
  font-size: 80%;
}

.fossil-toast {/* "toast"-style popup message */
  padding: 0.25em 0.5em;
  margin: 0;
  border-radius: 0.25em;
  font-size: 1em;
  opacity: 0.8;
  border-size: 1px;
  border-style: dotted;
  border-color: rgb( 127, 127, 127, 0.5 );
}
Changes to src/fossil.copybutton.js.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(function(F/*fossil object*/){
  /**
     A basic API for creating and managing a copy-to-clipboard button.

     Requires: fossil.bootstrap, fossil.dom
  */
  const D = F.dom;

  const config = {
    blinkTimeMs: 400
  };

  /**
     Initializes element e as a copy button using the given options
     object.

     The first argument may be a DOM element or a string (CSS selector
     suitable for use with document.querySelector()).









<
<
<
<







1
2
3
4
5
6
7
8




9
10
11
12
13
14
15
(function(F/*fossil object*/){
  /**
     A basic API for creating and managing a copy-to-clipboard button.

     Requires: fossil.bootstrap, fossil.dom
  */
  const D = F.dom;





  /**
     Initializes element e as a copy button using the given options
     object.

     The first argument may be a DOM element or a string (CSS selector
     suitable for use with document.querySelector()).

38
39
40
41
42
43
44





45
46
47
48
49
50
51
52
53
54









55
56
57
58




59
60
61
62
63
64
65
     function!).

     .cssClass: optional CSS class, or list of classes, to apply to e.

     .style: optional object of properties to copy directly into
     e.style.







     Note that this function's own defaultOptions object holds default
     values for some options. Any changes made to that object affect
     any future calls to this function.

     Be aware that clipboard functionality might or might not be
     available in any given environment. If this button appears to
     have no effect, that may be because it is not enabled/available
     in the current platform.










     Example:

     fossil.copyButton('#my-copy-button', {
       copyFromId: 'some-other-element-id'




     });
  */
  F.copyButton = function f(e, opt){
    if('string'===typeof e){
      e = document.querySelector(e);
    }    
    opt = F.mergeLastWins(f.defaultOptions, opt);







>
>
>
>
>










>
>
>
>
>
>
>
>
>


|

>
>
>
>







34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
     function!).

     .cssClass: optional CSS class, or list of classes, to apply to e.

     .style: optional object of properties to copy directly into
     e.style.

     .oncopy: an optional callback function which is added as an event
     listener for the 'text-copied' event (see below). There is
     functionally no difference from setting this option or adding a
     'text-copied' event listener to the element, and this option is
     considered to be a convenience form of that.

     Note that this function's own defaultOptions object holds default
     values for some options. Any changes made to that object affect
     any future calls to this function.

     Be aware that clipboard functionality might or might not be
     available in any given environment. If this button appears to
     have no effect, that may be because it is not enabled/available
     in the current platform.

     The copy button emits custom event 'text-copied' after it has
     successfully copied text to the clipboard. The event's "detail"
     member is an object with a "text" property holding the copied
     text. Other properties may be added in the future. The event is
     not fired if copying to the clipboard fails (e.g. is not
     available in the current environment).

     Returns the copy-initialized element.

     Example:

     const button = fossil.copyButton('#my-copy-button', {
       copyFromId: 'some-other-element-id'
     });
     button.addEventListener('text-copied',function(ev){
       fossil.dom.flashOnce(ev.target);
       console.debug("Copied text:",ev.detail.text);
     });
  */
  F.copyButton = function f(e, opt){
    if('string'===typeof e){
      e = document.querySelector(e);
    }    
    opt = F.mergeLastWins(f.defaultOptions, opt);
76
77
78
79
80
81
82
83
84

85

86
87
88
89




90
91
92
93
94
95
96
97
      undefined===srcElem.value ? ()=>srcElem.innerText : ()=>srcElem.value
    );
    D.copyStyle(e, opt.style);
    e.addEventListener(
      'click',
      function(){
        const txt = extract.call(opt);
        //console.debug("extracted ",txt);
        if(txt && D.copyTextToClipboard(txt)){

          D.flashOnce(e, config.blinkTimeMs);

        }
      },
      false
    );




  };

  F.copyButton.defaultOptions = {
    cssClass: 'copy-button',
    style: {/*properties copied as-is into element.style*/}
  };
  
})(window.fossil);







<

>
|
>




>
>
>
>








90
91
92
93
94
95
96

97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
      undefined===srcElem.value ? ()=>srcElem.innerText : ()=>srcElem.value
    );
    D.copyStyle(e, opt.style);
    e.addEventListener(
      'click',
      function(){
        const txt = extract.call(opt);

        if(txt && D.copyTextToClipboard(txt)){
          e.dispatchEvent(new CustomEvent('text-copied',{
            detail: {text: txt}
          }));
        }
      },
      false
    );
    if('function' === typeof opt.oncopy){
      e.addEventListener('text-copied', opt.oncopy, false);
    }
    return e;
  };

  F.copyButton.defaultOptions = {
    cssClass: 'copy-button',
    style: {/*properties copied as-is into element.style*/}
  };
  
})(window.fossil);
Changes to src/fossil.dom.js.
478
479
480
481
482
483
484



485
486
487
488
489
490
491




492
493
494
495
496
497
498
     in, it must be a function, and it gets callback back at the end
     of the asynchronous flashing processes.

     This will only activate once per element during that timeframe -
     further calls will become no-ops until the blink is
     completed. This routine adds a dataset member to the element for
     the duration of the blink, to allow it to block multiple blinks.




     Returns e, noting that the flash itself is asynchronous and may
     still be running, or not yet started, when this function returns.
  */
  dom.flashOnce = function f(e,howLongMs,afterFlashCallback){
    if(e.dataset.isBlinking){
      return;




    }
    if(!howLongMs || 'number'!==typeof howLongMs){
      howLongMs = f.defaultTimeMs;
    }
    e.dataset.isBlinking = true;
    const transition = e.style.transition;
    e.style.transition = "opacity "+howLongMs+"ms ease-in-out";







>
>
>







>
>
>
>







478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
     in, it must be a function, and it gets callback back at the end
     of the asynchronous flashing processes.

     This will only activate once per element during that timeframe -
     further calls will become no-ops until the blink is
     completed. This routine adds a dataset member to the element for
     the duration of the blink, to allow it to block multiple blinks.

     If passed 2 arguments and the 2nd is a function, it behaves as if
     it were called as (arg1, undefined, arg2).

     Returns e, noting that the flash itself is asynchronous and may
     still be running, or not yet started, when this function returns.
  */
  dom.flashOnce = function f(e,howLongMs,afterFlashCallback){
    if(e.dataset.isBlinking){
      return;
    }
    if(2===arguments.length && 'function' ===typeof howLongMs){
      afterFlashCallback = howLongMs;
      howLongMs = f.defaultTimeMs;
    }
    if(!howLongMs || 'number'!==typeof howLongMs){
      howLongMs = f.defaultTimeMs;
    }
    e.dataset.isBlinking = true;
    const transition = e.style.transition;
    e.style.transition = "opacity "+howLongMs+"ms ease-in-out";
Changes to src/fossil.numbered-lines.js.
47
48
49
50
51
52
53
54
55




56
57
58
59
60
61
62
    init: function(){
      const e = this.e;
      const btnCopy = D.span(),
            link = D.span();
      this.state = {link};
      F.copyButton(btnCopy,{
        copyFromElement: link,
        extractText: ()=>link.dataset.url
      });




      D.append(this.e, btnCopy, link)
    }
  });

  tbl.addEventListener('click', function f(ev){
    lineTip.show(false);
  }, false);







|
|
>
>
>
>







47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
    init: function(){
      const e = this.e;
      const btnCopy = D.span(),
            link = D.span();
      this.state = {link};
      F.copyButton(btnCopy,{
        copyFromElement: link,
        extractText: ()=>link.dataset.url,
        oncopy: (ev)=>{
          F.toast("Copied: ",D.append(D.code(),ev.detail.text));
          D.flashOnce(ev.target, undefined, ()=>lineTip.hide());
        }
      });//.addEventListener('text-copied', (ev)=>D.flashOnce(ev.target));
      D.append(this.e, btnCopy, link)
    }
  });

  tbl.addEventListener('click', function f(ev){
    lineTip.show(false);
  }, false);
Changes to src/fossil.popupwidget.js.
12
13
14
15
16
17
18
19

20
21
22
23
24
25
26
27
     Creates a new tooltip-like widget using the given options object.

     Options:

     .refresh: callback which is called just before the tooltip is
     revealed or moved. It must refresh the contents of the tooltip,
     if needed, by applying the content to/within this.e, which is the
     base DOM element for the tooltip. If the contents are static and

     set up via the .init option then this callback is not needed.

     .adjustX: an optional callback which is called when the tooltip
     is to be displayed at a given position and passed the X
     viewport-relative coordinate. This routine must either return its
     argument as-is or return an adjusted value. The intent is to
     allow a given tooltip may be positioned more appropriately for a
     given context, if needed (noting that the desired position can,







|
>
|







12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
     Creates a new tooltip-like widget using the given options object.

     Options:

     .refresh: callback which is called just before the tooltip is
     revealed or moved. It must refresh the contents of the tooltip,
     if needed, by applying the content to/within this.e, which is the
     base DOM element for the tooltip (and is a child of
     document.body). If the contents are static and set up via the
     .init option then this callback is not needed.

     .adjustX: an optional callback which is called when the tooltip
     is to be displayed at a given position and passed the X
     viewport-relative coordinate. This routine must either return its
     argument as-is or return an adjusted value. The intent is to
     allow a given tooltip may be positioned more appropriately for a
     given context, if needed (noting that the desired position can,
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
       Returns this object.

       Sidebar: showing/hiding the widget is, as is conventional for
       this framework, done by removing/adding the 'hidden' CSS class
       to it, so that class must be defined appropriately.
    */
    show: function(){
      var x = 0, y = 0, showIt;
      if(2===arguments.length){
        x = arguments[0];
        y = arguments[1];
        showIt = true;
      }else if(1===arguments.length){
        if(arguments[0] instanceof HTMLElement){
          const p = arguments[0];







|







143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
       Returns this object.

       Sidebar: showing/hiding the widget is, as is conventional for
       this framework, done by removing/adding the 'hidden' CSS class
       to it, so that class must be defined appropriately.
    */
    show: function(){
      var x = undefined, y = undefined, showIt;
      if(2===arguments.length){
        x = arguments[0];
        y = arguments[1];
        showIt = true;
      }else if(1===arguments.length){
        if(arguments[0] instanceof HTMLElement){
          const p = arguments[0];
165
166
167
168
169
170
171
172
173

174
175
176






177

178

179
180







































181
      if(showIt){
        this.refresh();
        x = this.options.adjustX.call(this,x);
        y = this.options.adjustY.call(this,y);
        x += window.pageXOffset;
        y += window.pageYOffset;
      }
      D[showIt ? 'removeClass' : 'addClass'](this.e, 'hidden');
      if(x || y){

        this.e.style.left = x+"px";
        this.e.style.top = y+"px";
      }






      return this;

    }

  }/*F.PopupWidget.prototype*/;
  







































})(window.fossil);







|
|
>
|
|
|
>
>
>
>
>
>

>
|
>

|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
      if(showIt){
        this.refresh();
        x = this.options.adjustX.call(this,x);
        y = this.options.adjustY.call(this,y);
        x += window.pageXOffset;
        y += window.pageYOffset;
      }
      console.debug("showIt?",showIt,x,y);
      if(showIt){
        if('number'===typeof x && 'number'===typeof y){
          this.e.style.left = x+"px";
          this.e.style.top = y+"px";
        }
        D.removeClass(this.e, 'hidden');
      }else{
        D.addClass(this.e, 'hidden');
        delete this.e.style.removeProperty('left');
        delete this.e.style.removeProperty('top');
      }
      return this;
    },

    hide: function(){return this.show(false)}
  }/*F.PopupWidget.prototype*/;

  /**
     Convenience wrapper around a PopupWidget which pops up a shared
     PopupWidget instance to show toast-style messages (commonly seen
     on Android). Its arguments may be anything suitable for passing
     to fossil.dom.append(), and each argument is first append()ed to
     the toast widget, then the widget is shown for
     F.toast.config.displayTimeMs milliseconds. This is called while
     a toast is currently being displayed, the first will be overwritten
     and the time until the message is hidden will be reset.

     The toast is always shown at the viewport-relative coordinates
     defined by the F.toast.config.position.

     The toaster's DOM element has the CSS classes fossil-tooltip
     and fossil-toast, so can be style via those.
  */
  F.toast = function f(/*...*/){
    if(!f.toast){
      f.toast = function ff(argsObject){
        if(!ff.toaster) ff.toaster = new F.PopupWidget({
          cssClass: ['fossil-tooltip', 'fossil-toast']
        });
        if(f._timer) clearTimeout(f._timer);
        D.clearElement(ff.toaster.e);
        var i = 0;
        for( ; i < argsObject.length; ++i ){
          D.append(ff.toaster.e, argsObject[i]);
        };
        ff.toaster.show(f.config.position.x, f.config.position.y);
        f._timer = setTimeout(()=>ff.toaster.hide(), f.config.displayTimeMs);
      };
    }
    f.toast(arguments);
  };
  F.toast.config = {
    position: { x: 5, y: 5 /*viewport-relative, pixels*/ },
    displayTimeMs: 2500
  };

})(window.fossil);