Fossil

Check-in [f98a4f5c94]
Login

Check-in [f98a4f5c94]

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

Overview
Comment:/chat: do not show the Toggle Text Mode feature for messages with no text, e.g. image-only posts (resolves an unhandled exception). When text is toggled to the unparsed state, show a copy-to-clipboard button which copies the raw message text to the clipboard. That is a workaround for mouse-copying of that text collecting extraneous newlines for reasons only the browsers understand.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: f98a4f5c94a844dd270526f380d8f6aaa641ebcf6b18feed5681b80b249aee23
User & Date: stephan 2022-06-08 15:52:08.791
References
2022-06-20
22:18
/chat: added a missing JS dependency which broke the new text-toggle/copy feature. Bug introduced in [f98a4f5c94a844dd], caused by failure to check in one of the associated files. ... (check-in: eeacf82158 user: stephan tags: trunk)
Context
2022-06-08
23:27
Merge in pikchrshow-wasm branch: reimplement /pikchrshow using a client-side WASM build of pikchr.c, plus related feature-adjacent tweaks in mimetype handling. ... (check-in: 7fcb462680 user: stephan tags: trunk)
15:52
/chat: do not show the Toggle Text Mode feature for messages with no text, e.g. image-only posts (resolves an unhandled exception). When text is toggled to the unparsed state, show a copy-to-clipboard button which copies the raw message text to the clipboard. That is a workaround for mouse-copying of that text collecting extraneous newlines for reasons only the browsers understand. ... (check-in: f98a4f5c94 user: stephan tags: trunk)
15:17
In /chat, change the EOL whitespace-stripping policy to retain up to 2 spaces, only stripping after the 3rd, to avoid breaking certain markdown constructs. Per /chat discussion. ... (check-in: cd7f2ddc98 user: stephan tags: trunk)
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/fossil.copybutton.js.
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34





35
36
37
38
39
40
41

     Options:

     .copyFromElement: DOM element

     .copyFromId: DOM element ID

     One of copyFromElement or copyFromId must be provided, but copyFromId
     may optionally be provided via e.dataset.copyFromId.

     .extractText: optional callback which is triggered when the copy
     button is clicked. It must return the text to copy to the
     clipboard. The default is to extract it from the copy-from
     element, using its [value] member, if it has one, else its
     [innerText]. A client-provided callback may use any data source
     it likes, so long as it's synchronous. If this function returns a
     falsy value then the clipboard is not modified. This function is
     called with the fully expanded/resolved options object as its
     "this" (that's a different instance than the one passed to this
     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







<
<
<










>
>
>
>
>







15
16
17
18
19
20
21



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

     Options:

     .copyFromElement: DOM element

     .copyFromId: DOM element ID




     .extractText: optional callback which is triggered when the copy
     button is clicked. It must return the text to copy to the
     clipboard. The default is to extract it from the copy-from
     element, using its [value] member, if it has one, else its
     [innerText]. A client-provided callback may use any data source
     it likes, so long as it's synchronous. If this function returns a
     falsy value then the clipboard is not modified. This function is
     called with the fully expanded/resolved options object as its
     "this" (that's a different instance than the one passed to this
     function!).

     At least one of copyFromElement, copyFromId, or extractText must
     be provided, but if copyFromId is not set and e.dataset.copyFromId
     is then that value is used in its place. extractText() trumps the
     other two options.

     .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
Changes to src/fossil.page.chat.js.
700
701
702
703
704
705
706





707
708
709
710
711
712
713
714
715
716
717
718

















719
720
721
722
723
724
725
726
727
728
729

730
731
732
733
734
735
736
      }else{
        e = this.getMessageElemById(id);
      }
      if(!e || !id) return false;
      else if(e.$isToggling) return;
      e.$isToggling = true;
      const content = e.querySelector('.content-target');





      if(!content.$elems){
        content.$elems = [
          content.firstElementChild, // parsed elem
          undefined // plaintext elem
        ];
      }else if(content.$elems[1]){
        // We have both content types. Simply toggle them.
        const child = (
          content.firstElementChild===content.$elems[0]
            ? content.$elems[1]
            : content.$elems[0]
        );

















        delete e.$isToggling;
        D.append(D.clearElement(content), child);
        return;
      }
      // We need to fetch the plain-text version...
      const self = this;
      F.fetch('chat-fetch-one',{
        urlParams:{ name: id, raw: true},
        responseType: 'json',
        onload: function(msg){
          content.$elems[1] = D.append(D.pre(),msg.xmsg);

          self.toggleTextMode(e);
        },
        aftersend:function(){
          delete e.$isToggling;
          Chat.ajaxEnd();
        }
      });







>
>
>
>
>












>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

|









>







700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
      }else{
        e = this.getMessageElemById(id);
      }
      if(!e || !id) return false;
      else if(e.$isToggling) return;
      e.$isToggling = true;
      const content = e.querySelector('.content-target');
      if(!content){
        console.warn("Should not be possible: trying to toggle text",
                     "mode of a message with no .content-target.", e);
        return;
      }
      if(!content.$elems){
        content.$elems = [
          content.firstElementChild, // parsed elem
          undefined // plaintext elem
        ];
      }else if(content.$elems[1]){
        // We have both content types. Simply toggle them.
        const child = (
          content.firstElementChild===content.$elems[0]
            ? content.$elems[1]
            : content.$elems[0]
        );
        D.clearElement(content);
        if(child===content.$elems[1]){
          /* When showing the unformatted version, inject a
             copy-to-clipboard button. This is a workaround for
             mouse-copying from that field collecting twice as many
             newlines as it should (for unknown reasons). */
          const cpId = 'copy-to-clipboard-'+id;
          /* ^^^ copy button element ID, needed for LABEL element
             pairing.  Recall that we destroy all child elements of
             `content` each time we hit this block, so we can reuse
             that element ID on subsequent toggles. */
          const btnCp = D.attr(D.addClass(D.span(),'copy-button'), 'id', cpId);
          F.copyButton(btnCp, {extractText: ()=>child._xmsgRaw});
          const lblCp = D.label(cpId, "Copy unformatted text");
          lblCp.addEventListener('click',()=>btnCp.click(), false);
          D.append(content, D.append(D.addClass(D.span(), 'nobr'), btnCp, lblCp));
        }
        delete e.$isToggling;
        D.append(content, child);
        return;
      }
      // We need to fetch the plain-text version...
      const self = this;
      F.fetch('chat-fetch-one',{
        urlParams:{ name: id, raw: true},
        responseType: 'json',
        onload: function(msg){
          content.$elems[1] = D.append(D.pre(),msg.xmsg);
          content.$elems[1]._xmsgRaw = msg.xmsg/*used for copy-to-clipboard feature*/;
          self.toggleTextMode(e);
        },
        aftersend:function(){
          delete e.$isToggling;
          Chat.ajaxEnd();
        }
      });
1133
1134
1135
1136
1137
1138
1139



1140
1141
1142
1143
1144

1145
1146
1147
1148
1149
1150
1151
                    e = n;
                  }
                  eMsg.scrollIntoView();
                }
              ));
              const toolbar2 = D.addClass(D.div(), 'toolbar');
              D.append(this.e, toolbar2);



              D.append(toolbar2, D.button(
                "Toggle text mode", function(){
                  self.hide();
                  Chat.toggleTextMode(eMsg);
                }));

              if(eMsg.dataset.xfrom){
                /* Add a link to the /timeline filtered on this user. */
                const timelineLink = D.attr(
                  D.a(F.repoUrl('timeline',{
                    u: eMsg.dataset.xfrom,
                    y: 'a'
                  }), "User's Timeline"),







>
>
>
|
|
|
|
|
>







1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
                    e = n;
                  }
                  eMsg.scrollIntoView();
                }
              ));
              const toolbar2 = D.addClass(D.div(), 'toolbar');
              D.append(this.e, toolbar2);
              if(eMsg.querySelector('.content-target')){
                /* ^^^ messages with only an embedded image have no
                   .content-target area. */
                D.append(toolbar2, D.button(
                  "Toggle text mode", function(){
                    self.hide();
                    Chat.toggleTextMode(eMsg);
                  }));
              }
              if(eMsg.dataset.xfrom){
                /* Add a link to the /timeline filtered on this user. */
                const timelineLink = D.attr(
                  D.a(F.repoUrl('timeline',{
                    u: eMsg.dataset.xfrom,
                    y: 'a'
                  }), "User's Timeline"),