Fossil SCM

/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.

stephan 2022-06-08 15:52 trunk
Commit f98a4f5c94a844dd270526f380d8f6aaa641ebcf6b18feed5681b80b249aee23
--- src/fossil.copybutton.js
+++ src/fossil.copybutton.js
@@ -17,13 +17,10 @@
1717
1818
.copyFromElement: DOM element
1919
2020
.copyFromId: DOM element ID
2121
22
- One of copyFromElement or copyFromId must be provided, but copyFromId
23
- may optionally be provided via e.dataset.copyFromId.
24
-
2522
.extractText: optional callback which is triggered when the copy
2623
button is clicked. It must return the text to copy to the
2724
clipboard. The default is to extract it from the copy-from
2825
element, using its [value] member, if it has one, else its
2926
[innerText]. A client-provided callback may use any data source
@@ -30,10 +27,15 @@
3027
it likes, so long as it's synchronous. If this function returns a
3128
falsy value then the clipboard is not modified. This function is
3229
called with the fully expanded/resolved options object as its
3330
"this" (that's a different instance than the one passed to this
3431
function!).
32
+
33
+ At least one of copyFromElement, copyFromId, or extractText must
34
+ be provided, but if copyFromId is not set and e.dataset.copyFromId
35
+ is then that value is used in its place. extractText() trumps the
36
+ other two options.
3537
3638
.cssClass: optional CSS class, or list of classes, to apply to e.
3739
3840
.style: optional object of properties to copy directly into
3941
e.style.
4042
--- src/fossil.copybutton.js
+++ src/fossil.copybutton.js
@@ -17,13 +17,10 @@
17
18 .copyFromElement: DOM element
19
20 .copyFromId: DOM element ID
21
22 One of copyFromElement or copyFromId must be provided, but copyFromId
23 may optionally be provided via e.dataset.copyFromId.
24
25 .extractText: optional callback which is triggered when the copy
26 button is clicked. It must return the text to copy to the
27 clipboard. The default is to extract it from the copy-from
28 element, using its [value] member, if it has one, else its
29 [innerText]. A client-provided callback may use any data source
@@ -30,10 +27,15 @@
30 it likes, so long as it's synchronous. If this function returns a
31 falsy value then the clipboard is not modified. This function is
32 called with the fully expanded/resolved options object as its
33 "this" (that's a different instance than the one passed to this
34 function!).
 
 
 
 
 
35
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
--- src/fossil.copybutton.js
+++ src/fossil.copybutton.js
@@ -17,13 +17,10 @@
17
18 .copyFromElement: DOM element
19
20 .copyFromId: DOM element ID
21
 
 
 
22 .extractText: optional callback which is triggered when the copy
23 button is clicked. It must return the text to copy to the
24 clipboard. The default is to extract it from the copy-from
25 element, using its [value] member, if it has one, else its
26 [innerText]. A client-provided callback may use any data source
@@ -30,10 +27,15 @@
27 it likes, so long as it's synchronous. If this function returns a
28 falsy value then the clipboard is not modified. This function is
29 called with the fully expanded/resolved options object as its
30 "this" (that's a different instance than the one passed to this
31 function!).
32
33 At least one of copyFromElement, copyFromId, or extractText must
34 be provided, but if copyFromId is not set and e.dataset.copyFromId
35 is then that value is used in its place. extractText() trumps the
36 other two options.
37
38 .cssClass: optional CSS class, or list of classes, to apply to e.
39
40 .style: optional object of properties to copy directly into
41 e.style.
42
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -702,10 +702,15 @@
702702
}
703703
if(!e || !id) return false;
704704
else if(e.$isToggling) return;
705705
e.$isToggling = true;
706706
const content = e.querySelector('.content-target');
707
+ if(!content){
708
+ console.warn("Should not be possible: trying to toggle text",
709
+ "mode of a message with no .content-target.", e);
710
+ return;
711
+ }
707712
if(!content.$elems){
708713
content.$elems = [
709714
content.firstElementChild, // parsed elem
710715
undefined // plaintext elem
711716
];
@@ -714,21 +719,39 @@
714719
const child = (
715720
content.firstElementChild===content.$elems[0]
716721
? content.$elems[1]
717722
: content.$elems[0]
718723
);
724
+ D.clearElement(content);
725
+ if(child===content.$elems[1]){
726
+ /* When showing the unformatted version, inject a
727
+ copy-to-clipboard button. This is a workaround for
728
+ mouse-copying from that field collecting twice as many
729
+ newlines as it should (for unknown reasons). */
730
+ const cpId = 'copy-to-clipboard-'+id;
731
+ /* ^^^ copy button element ID, needed for LABEL element
732
+ pairing. Recall that we destroy all child elements of
733
+ `content` each time we hit this block, so we can reuse
734
+ that element ID on subsequent toggles. */
735
+ const btnCp = D.attr(D.addClass(D.span(),'copy-button'), 'id', cpId);
736
+ F.copyButton(btnCp, {extractText: ()=>child._xmsgRaw});
737
+ const lblCp = D.label(cpId, "Copy unformatted text");
738
+ lblCp.addEventListener('click',()=>btnCp.click(), false);
739
+ D.append(content, D.append(D.addClass(D.span(), 'nobr'), btnCp, lblCp));
740
+ }
719741
delete e.$isToggling;
720
- D.append(D.clearElement(content), child);
742
+ D.append(content, child);
721743
return;
722744
}
723745
// We need to fetch the plain-text version...
724746
const self = this;
725747
F.fetch('chat-fetch-one',{
726748
urlParams:{ name: id, raw: true},
727749
responseType: 'json',
728750
onload: function(msg){
729751
content.$elems[1] = D.append(D.pre(),msg.xmsg);
752
+ content.$elems[1]._xmsgRaw = msg.xmsg/*used for copy-to-clipboard feature*/;
730753
self.toggleTextMode(e);
731754
},
732755
aftersend:function(){
733756
delete e.$isToggling;
734757
Chat.ajaxEnd();
@@ -1135,15 +1158,19 @@
11351158
eMsg.scrollIntoView();
11361159
}
11371160
));
11381161
const toolbar2 = D.addClass(D.div(), 'toolbar');
11391162
D.append(this.e, toolbar2);
1140
- D.append(toolbar2, D.button(
1141
- "Toggle text mode", function(){
1142
- self.hide();
1143
- Chat.toggleTextMode(eMsg);
1144
- }));
1163
+ if(eMsg.querySelector('.content-target')){
1164
+ /* ^^^ messages with only an embedded image have no
1165
+ .content-target area. */
1166
+ D.append(toolbar2, D.button(
1167
+ "Toggle text mode", function(){
1168
+ self.hide();
1169
+ Chat.toggleTextMode(eMsg);
1170
+ }));
1171
+ }
11451172
if(eMsg.dataset.xfrom){
11461173
/* Add a link to the /timeline filtered on this user. */
11471174
const timelineLink = D.attr(
11481175
D.a(F.repoUrl('timeline',{
11491176
u: eMsg.dataset.xfrom,
11501177
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -702,10 +702,15 @@
702 }
703 if(!e || !id) return false;
704 else if(e.$isToggling) return;
705 e.$isToggling = true;
706 const content = e.querySelector('.content-target');
 
 
 
 
 
707 if(!content.$elems){
708 content.$elems = [
709 content.firstElementChild, // parsed elem
710 undefined // plaintext elem
711 ];
@@ -714,21 +719,39 @@
714 const child = (
715 content.firstElementChild===content.$elems[0]
716 ? content.$elems[1]
717 : content.$elems[0]
718 );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
719 delete e.$isToggling;
720 D.append(D.clearElement(content), child);
721 return;
722 }
723 // We need to fetch the plain-text version...
724 const self = this;
725 F.fetch('chat-fetch-one',{
726 urlParams:{ name: id, raw: true},
727 responseType: 'json',
728 onload: function(msg){
729 content.$elems[1] = D.append(D.pre(),msg.xmsg);
 
730 self.toggleTextMode(e);
731 },
732 aftersend:function(){
733 delete e.$isToggling;
734 Chat.ajaxEnd();
@@ -1135,15 +1158,19 @@
1135 eMsg.scrollIntoView();
1136 }
1137 ));
1138 const toolbar2 = D.addClass(D.div(), 'toolbar');
1139 D.append(this.e, toolbar2);
1140 D.append(toolbar2, D.button(
1141 "Toggle text mode", function(){
1142 self.hide();
1143 Chat.toggleTextMode(eMsg);
1144 }));
 
 
 
 
1145 if(eMsg.dataset.xfrom){
1146 /* Add a link to the /timeline filtered on this user. */
1147 const timelineLink = D.attr(
1148 D.a(F.repoUrl('timeline',{
1149 u: eMsg.dataset.xfrom,
1150
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -702,10 +702,15 @@
702 }
703 if(!e || !id) return false;
704 else if(e.$isToggling) return;
705 e.$isToggling = true;
706 const content = e.querySelector('.content-target');
707 if(!content){
708 console.warn("Should not be possible: trying to toggle text",
709 "mode of a message with no .content-target.", e);
710 return;
711 }
712 if(!content.$elems){
713 content.$elems = [
714 content.firstElementChild, // parsed elem
715 undefined // plaintext elem
716 ];
@@ -714,21 +719,39 @@
719 const child = (
720 content.firstElementChild===content.$elems[0]
721 ? content.$elems[1]
722 : content.$elems[0]
723 );
724 D.clearElement(content);
725 if(child===content.$elems[1]){
726 /* When showing the unformatted version, inject a
727 copy-to-clipboard button. This is a workaround for
728 mouse-copying from that field collecting twice as many
729 newlines as it should (for unknown reasons). */
730 const cpId = 'copy-to-clipboard-'+id;
731 /* ^^^ copy button element ID, needed for LABEL element
732 pairing. Recall that we destroy all child elements of
733 `content` each time we hit this block, so we can reuse
734 that element ID on subsequent toggles. */
735 const btnCp = D.attr(D.addClass(D.span(),'copy-button'), 'id', cpId);
736 F.copyButton(btnCp, {extractText: ()=>child._xmsgRaw});
737 const lblCp = D.label(cpId, "Copy unformatted text");
738 lblCp.addEventListener('click',()=>btnCp.click(), false);
739 D.append(content, D.append(D.addClass(D.span(), 'nobr'), btnCp, lblCp));
740 }
741 delete e.$isToggling;
742 D.append(content, child);
743 return;
744 }
745 // We need to fetch the plain-text version...
746 const self = this;
747 F.fetch('chat-fetch-one',{
748 urlParams:{ name: id, raw: true},
749 responseType: 'json',
750 onload: function(msg){
751 content.$elems[1] = D.append(D.pre(),msg.xmsg);
752 content.$elems[1]._xmsgRaw = msg.xmsg/*used for copy-to-clipboard feature*/;
753 self.toggleTextMode(e);
754 },
755 aftersend:function(){
756 delete e.$isToggling;
757 Chat.ajaxEnd();
@@ -1135,15 +1158,19 @@
1158 eMsg.scrollIntoView();
1159 }
1160 ));
1161 const toolbar2 = D.addClass(D.div(), 'toolbar');
1162 D.append(this.e, toolbar2);
1163 if(eMsg.querySelector('.content-target')){
1164 /* ^^^ messages with only an embedded image have no
1165 .content-target area. */
1166 D.append(toolbar2, D.button(
1167 "Toggle text mode", function(){
1168 self.hide();
1169 Chat.toggleTextMode(eMsg);
1170 }));
1171 }
1172 if(eMsg.dataset.xfrom){
1173 /* Add a link to the /timeline filtered on this user. */
1174 const timelineLink = D.attr(
1175 D.a(F.repoUrl('timeline',{
1176 u: eMsg.dataset.xfrom,
1177

Keyboard Shortcuts

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