Fossil SCM

Implement bottom-up and top-down chat layouts in chat-only mode and normal mode. There is a minor scroll-on-new-message quirk or two to resolve, but it otherwise seems to work.

stephan 2020-12-26 15:40 chat-mode-bottom-up
Commit dfc20f429713e4c46e26a6d0d421162260314f32322926b6fb3dc5a88868f11a
+3 -1
--- src/chat.c
+++ src/chat.c
@@ -503,11 +503,13 @@
503503
/*
504504
** WEBPAGE: chat-download
505505
**
506506
** Download the CHAT.FILE attachment associated with a single chat
507507
** entry. The "name" query parameter begins with an integer that
508
-** identifies the particular chat message.
508
+** identifies the particular chat message. The integer may be followed
509
+** by a / and a filename, which will indicate to the browser to use
510
+** the indicated name when saving the file.
509511
*/
510512
void chat_download_webpage(void){
511513
int msgid;
512514
Blob r;
513515
const char *zMime;
514516
--- src/chat.c
+++ src/chat.c
@@ -503,11 +503,13 @@
503 /*
504 ** WEBPAGE: chat-download
505 **
506 ** Download the CHAT.FILE attachment associated with a single chat
507 ** entry. The "name" query parameter begins with an integer that
508 ** identifies the particular chat message.
 
 
509 */
510 void chat_download_webpage(void){
511 int msgid;
512 Blob r;
513 const char *zMime;
514
--- src/chat.c
+++ src/chat.c
@@ -503,11 +503,13 @@
503 /*
504 ** WEBPAGE: chat-download
505 **
506 ** Download the CHAT.FILE attachment associated with a single chat
507 ** entry. The "name" query parameter begins with an integer that
508 ** identifies the particular chat message. The integer may be followed
509 ** by a / and a filename, which will indicate to the browser to use
510 ** the indicated name when saving the file.
511 */
512 void chat_download_webpage(void){
513 int msgid;
514 Blob r;
515 const char *zMime;
516
+67 -30
--- src/chat.js
+++ src/chat.js
@@ -21,11 +21,12 @@
2121
inputForm: E1('#chat-form'),
2222
btnSubmit: E1('#chat-message-submit'),
2323
inputSingle: E1('#chat-input-single'),
2424
inputMulti: E1('#chat-input-multi'),
2525
inputCurrent: undefined/*one of inputSingle or inputMulti*/,
26
- inputFile: E1('#chat-input-file')
26
+ inputFile: E1('#chat-input-file'),
27
+ contentDiv: E1('div.content')
2728
},
2829
me: F.user.name,
2930
mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
3031
mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
3132
pageIsActive: 'visible'===document.visibilityState,
@@ -64,10 +65,11 @@
6465
this.e.inputCurrent = this.e.inputSingle;
6566
}
6667
D.addClass(old, 'hidden');
6768
D.removeClass(this.e.inputCurrent, 'hidden');
6869
this.e.inputCurrent.value = old.value;
70
+ old.value = '';
6971
return this;
7072
},
7173
/** Enables (if yes is truthy) or disables all elements in
7274
* this.disableDuringAjax. */
7375
enableAjaxComponents: function(yes){
@@ -115,26 +117,34 @@
115117
if(atEnd){
116118
mip.parentNode.insertBefore(e, mip);
117119
}else{
118120
if(mip.nextSibling) mip.parentNode.insertBefore(e, mip.nextSibling);
119121
else mip.parentNode.appendChild(e);
120
- if(this.isChatOnlyMode()){
121
- e.scrollIntoView();
122
- }else{
123
- //const rect = e.getBoundingClientRect();
124
- //const rect = this.e.inputWrapper.getBoundingClientRect();
125
- //window.scrollBy(0, -cs.height);
126
- //console.debug("rect =",rect);
127
- //window.scrollBy(0,rect.height);
128
- //window.scrollTo(0,rect.top);
129
- //e.querySelector('.message-widget-tab').scrollIntoView();
122
+ if(this.isUiFlipped()){
123
+ /* When UI is flipped, new messages start out under the
124
+ text input area because of its position:sticky
125
+ style. We have to scroll them up. When the page footer
126
+ is not hidden but is not on-screen, this causes a
127
+ slight amount of UI jarring as the footer is *also*
128
+ scrolled into view (for whatever reason). */
129
+ setTimeout(()=>e.scrollIntoView(), 0);
130130
}
131131
}
132132
},
133
- isChatOnlyMode: function(){
134
- return document.body.classList.contains('chat-only-mode');
133
+ /** Returns true if chat-only mode is enabled. */
134
+ isChatOnlyMode: ()=>document.body.classList.contains('chat-only-mode'),
135
+ /** Returns true if the UI seems to be in "bottom-up" mode. */
136
+ isUiFlipped: function(){
137
+ const style = window.getComputedStyle(this.e.contentDiv);
138
+ return style.flexDirection.indexOf("-reverse")>0;
135139
},
140
+ /**
141
+ Enters (if passed a truthy value or no arguments) or leaves
142
+ "chat-only" mode. That mode hides the page's header and
143
+ footer, leaving only the chat application visible to the
144
+ user.
145
+ */
136146
chatOnlyMode: function f(yes){
137147
if(undefined === f.elemsToToggle){
138148
f.elemsToToggle = [];
139149
document.body.childNodes.forEach(function(e){
140150
if(!e.classList) return/*TEXT nodes and such*/;
@@ -153,15 +163,17 @@
153163
document.body.scroll(0,document.body.height);
154164
}else{
155165
D.removeClass(f.elemsToToggle, 'hidden');
156166
D.removeClass(document.body, 'chat-only-mode');
157167
setTimeout(()=>document.body.scrollIntoView(
158
- /*moves to (0,0), whereas scrollTo(0,0) does not!*/
168
+ /*moves to (0,0), whereas scrollTo(0,0) does not!
169
+ setTimeout() is unfortunately necessary to get the scroll
170
+ placement correct.*/
159171
), 0);
160172
}
161173
const msg = document.querySelector('.message-widget');
162
- if(msg) msg.scrollIntoView();
174
+ if(msg) setTimeout(()=>msg.scrollIntoView(),0);
163175
return this;
164176
},
165177
toggleChatOnlyMode: function(){
166178
return this.chatOnlyMode(!this.isChatOnlyMode());
167179
},
@@ -169,25 +181,31 @@
169181
get: (k,dflt)=>F.storage.get(k,dflt),
170182
getBool: (k,dflt)=>F.storage.getBool(k,dflt),
171183
set: (k,v)=>F.storage.set(k,v),
172184
defaults:{
173185
"images-inline": !!F.config.chat.imagesInline,
174
- "monospace-messages": false
186
+ "monospace-messages": false,
187
+ "bottom-up": true
175188
}
176189
}
177190
};
178
- Object.keys(cs.settings.defaults).forEach(function f(k){
179
- const v = cs.settings.get(k,f);
180
- if(f===v) cs.settings.set(k,cs.settings.defaults[k]);
191
+ /* Install default settings... */
192
+ Object.keys(cs.settings.defaults).forEach(function(k){
193
+ const v = cs.settings.get(k,cs);
194
+ if(cs===v) cs.settings.set(k,cs.settings.defaults[k]);
181195
});
182196
if(window.innerWidth<window.innerHeight){
183197
/* Alignment of 'my' messages: right alignment is conventional
184198
for mobile chat apps but can be difficult to read in wide
185
- windows (desktop/tablet landscape mode). Can be toggled via
186
- settings popup. */
199
+ windows (desktop/tablet landscape mode), so we default to a
200
+ layout based on the apparently "orientation" of the window:
201
+ tall vs wide. Can be toggled via settings popup. */
187202
document.body.classList.add('my-messages-right');
188203
}
204
+ if(cs.settings.getBool("bottom-up")){
205
+ document.body.classList.add('chat-bottom-up');
206
+ }
189207
if(cs.settings.getBool('monospace-messages',false)){
190208
document.body.classList.add('monospace-messages');
191209
}
192210
cs.e.inputCurrent = cs.e.inputSingle;
193211
cs.pageTitleOrig = cs.e.pageTitle.innerText;
@@ -327,12 +345,15 @@
327345
this.e.content.style.backgroundColor = m.uclr;
328346
this.e.tab.style.backgroundColor = m.uclr;
329347
330348
const d = new Date(m.mtime);
331349
D.append(
332
- D.clearElement(this.e.tab), D.text(
333
- m.xfrom+' @ '+d.getHours()+":"+(d.getMinutes()+100).toString().slice(1,3))
350
+ D.clearElement(this.e.tab),
351
+ D.text(
352
+ m.xfrom," #",m.msgid,' @ ',d.getHours(),":",
353
+ (d.getMinutes()+100).toString().slice(1,3)
354
+ )
334355
);
335356
var contentTarget = this.e.content;
336357
if( m.fsize>0 ){
337358
if( m.fmime
338359
&& m.fmime.startsWith("image/")
@@ -347,14 +368,17 @@
347368
"(" + m.fname + " " + m.fsize + " bytes)"
348369
)
349370
D.attr(a,'target','_blank');
350371
contentTarget.appendChild(a);
351372
}
352
- contentTarget = D.div();
373
+ ;
353374
}
354375
if(m.xmsg){
355
- if(contentTarget !== this.e.content){
376
+ if(m.fsize>0){
377
+ /* We have file/image content, so need another element for
378
+ the message text. */
379
+ contentTarget = D.div();
356380
D.append(this.e.content, contentTarget);
357381
}
358382
// The m.xmsg text comes from the same server as this script and
359383
// is guaranteed by that server to be "safe" HTML - safe in the
360384
// sense that it is not possible for a malefactor to inject HTML
@@ -552,11 +576,11 @@
552576
Chat.deleteMessage(eMsg);
553577
});
554578
}
555579
}/*refresh()*/
556580
});
557
- f.popup.installClickToHide();
581
+ f.popup.installHideHandlers();
558582
f.popup.hide = function(){
559583
delete this._eMsg;
560584
D.clearElement(this.e);
561585
return this.show(false);
562586
};
@@ -608,10 +632,18 @@
608632
label: "Left-align my posts",
609633
boolValue: ()=>!document.body.classList.contains('my-messages-right'),
610634
callback: function f(){
611635
document.body.classList.toggle('my-messages-right');
612636
}
637
+ },{
638
+ label: "Bottom-up chat",
639
+ boolValue: ()=>document.body.classList.contains('chat-bottom-up'),
640
+ callback: function(){
641
+ document.body.classList.toggle('chat-bottom-up');
642
+ Chat.settings.set('bottom-up',
643
+ document.body.classList.contains('chat-bottom-up'));
644
+ }
613645
},{
614646
label: "Images inline",
615647
boolValue: ()=>Chat.settings.getBool('images-inline'),
616648
callback: function(){
617649
const v = Chat.settings.getBool('images-inline',true);
@@ -642,13 +674,18 @@
642674
}
643675
D.append(settingsPopup.e, line);
644676
btn.addEventListener('click', callback);
645677
});
646678
};
647
- settingsPopup.installClickToHide()
648
- /** Reminder: that interferes with "?" embedded within the popup,
649
- so cannot be used together with those. */;
679
+ settingsPopup.installHideHandlers(false, true, true)
680
+ /** Reminder: click-to-hide interferes with "?" embedded within
681
+ the popup, so cannot be used together with those. Enabling
682
+ this means, however, that tapping the menu button to toggle
683
+ the menu cannot work because tapping the menu button while the
684
+ menu is opened will, because of the click-to-hide handler,
685
+ hide the menu before the button gets an event saying to toggle
686
+ it.*/;
650687
D.attr(settingsButton, 'role', 'button');
651688
settingsButton.addEventListener('click',function(ev){
652689
//ev.preventDefault();
653690
if(settingsPopup.isShown()) settingsPopup.hide();
654691
else settingsPopup.show(settingsButton);
@@ -665,11 +702,11 @@
665702
const rect = settingsButton.getBoundingClientRect();
666703
return rect.right - popupSize.width;
667704
};
668705
settingsPopup.options.adjustY = function(y){
669706
const rect = settingsButton.getBoundingClientRect();
670
- if(Chat.isChatOnlyMode()){
707
+ if(Chat.isUiFlipped()){
671708
return rect.top - popupSize.height -2;
672709
}else{
673710
return rect.bottom + 2;
674711
}
675712
};
676713
--- src/chat.js
+++ src/chat.js
@@ -21,11 +21,12 @@
21 inputForm: E1('#chat-form'),
22 btnSubmit: E1('#chat-message-submit'),
23 inputSingle: E1('#chat-input-single'),
24 inputMulti: E1('#chat-input-multi'),
25 inputCurrent: undefined/*one of inputSingle or inputMulti*/,
26 inputFile: E1('#chat-input-file')
 
27 },
28 me: F.user.name,
29 mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
30 mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
31 pageIsActive: 'visible'===document.visibilityState,
@@ -64,10 +65,11 @@
64 this.e.inputCurrent = this.e.inputSingle;
65 }
66 D.addClass(old, 'hidden');
67 D.removeClass(this.e.inputCurrent, 'hidden');
68 this.e.inputCurrent.value = old.value;
 
69 return this;
70 },
71 /** Enables (if yes is truthy) or disables all elements in
72 * this.disableDuringAjax. */
73 enableAjaxComponents: function(yes){
@@ -115,26 +117,34 @@
115 if(atEnd){
116 mip.parentNode.insertBefore(e, mip);
117 }else{
118 if(mip.nextSibling) mip.parentNode.insertBefore(e, mip.nextSibling);
119 else mip.parentNode.appendChild(e);
120 if(this.isChatOnlyMode()){
121 e.scrollIntoView();
122 }else{
123 //const rect = e.getBoundingClientRect();
124 //const rect = this.e.inputWrapper.getBoundingClientRect();
125 //window.scrollBy(0, -cs.height);
126 //console.debug("rect =",rect);
127 //window.scrollBy(0,rect.height);
128 //window.scrollTo(0,rect.top);
129 //e.querySelector('.message-widget-tab').scrollIntoView();
130 }
131 }
132 },
133 isChatOnlyMode: function(){
134 return document.body.classList.contains('chat-only-mode');
 
 
 
 
135 },
 
 
 
 
 
 
136 chatOnlyMode: function f(yes){
137 if(undefined === f.elemsToToggle){
138 f.elemsToToggle = [];
139 document.body.childNodes.forEach(function(e){
140 if(!e.classList) return/*TEXT nodes and such*/;
@@ -153,15 +163,17 @@
153 document.body.scroll(0,document.body.height);
154 }else{
155 D.removeClass(f.elemsToToggle, 'hidden');
156 D.removeClass(document.body, 'chat-only-mode');
157 setTimeout(()=>document.body.scrollIntoView(
158 /*moves to (0,0), whereas scrollTo(0,0) does not!*/
 
 
159 ), 0);
160 }
161 const msg = document.querySelector('.message-widget');
162 if(msg) msg.scrollIntoView();
163 return this;
164 },
165 toggleChatOnlyMode: function(){
166 return this.chatOnlyMode(!this.isChatOnlyMode());
167 },
@@ -169,25 +181,31 @@
169 get: (k,dflt)=>F.storage.get(k,dflt),
170 getBool: (k,dflt)=>F.storage.getBool(k,dflt),
171 set: (k,v)=>F.storage.set(k,v),
172 defaults:{
173 "images-inline": !!F.config.chat.imagesInline,
174 "monospace-messages": false
 
175 }
176 }
177 };
178 Object.keys(cs.settings.defaults).forEach(function f(k){
179 const v = cs.settings.get(k,f);
180 if(f===v) cs.settings.set(k,cs.settings.defaults[k]);
 
181 });
182 if(window.innerWidth<window.innerHeight){
183 /* Alignment of 'my' messages: right alignment is conventional
184 for mobile chat apps but can be difficult to read in wide
185 windows (desktop/tablet landscape mode). Can be toggled via
186 settings popup. */
 
187 document.body.classList.add('my-messages-right');
188 }
 
 
 
189 if(cs.settings.getBool('monospace-messages',false)){
190 document.body.classList.add('monospace-messages');
191 }
192 cs.e.inputCurrent = cs.e.inputSingle;
193 cs.pageTitleOrig = cs.e.pageTitle.innerText;
@@ -327,12 +345,15 @@
327 this.e.content.style.backgroundColor = m.uclr;
328 this.e.tab.style.backgroundColor = m.uclr;
329
330 const d = new Date(m.mtime);
331 D.append(
332 D.clearElement(this.e.tab), D.text(
333 m.xfrom+' @ '+d.getHours()+":"+(d.getMinutes()+100).toString().slice(1,3))
 
 
 
334 );
335 var contentTarget = this.e.content;
336 if( m.fsize>0 ){
337 if( m.fmime
338 && m.fmime.startsWith("image/")
@@ -347,14 +368,17 @@
347 "(" + m.fname + " " + m.fsize + " bytes)"
348 )
349 D.attr(a,'target','_blank');
350 contentTarget.appendChild(a);
351 }
352 contentTarget = D.div();
353 }
354 if(m.xmsg){
355 if(contentTarget !== this.e.content){
 
 
 
356 D.append(this.e.content, contentTarget);
357 }
358 // The m.xmsg text comes from the same server as this script and
359 // is guaranteed by that server to be "safe" HTML - safe in the
360 // sense that it is not possible for a malefactor to inject HTML
@@ -552,11 +576,11 @@
552 Chat.deleteMessage(eMsg);
553 });
554 }
555 }/*refresh()*/
556 });
557 f.popup.installClickToHide();
558 f.popup.hide = function(){
559 delete this._eMsg;
560 D.clearElement(this.e);
561 return this.show(false);
562 };
@@ -608,10 +632,18 @@
608 label: "Left-align my posts",
609 boolValue: ()=>!document.body.classList.contains('my-messages-right'),
610 callback: function f(){
611 document.body.classList.toggle('my-messages-right');
612 }
 
 
 
 
 
 
 
 
613 },{
614 label: "Images inline",
615 boolValue: ()=>Chat.settings.getBool('images-inline'),
616 callback: function(){
617 const v = Chat.settings.getBool('images-inline',true);
@@ -642,13 +674,18 @@
642 }
643 D.append(settingsPopup.e, line);
644 btn.addEventListener('click', callback);
645 });
646 };
647 settingsPopup.installClickToHide()
648 /** Reminder: that interferes with "?" embedded within the popup,
649 so cannot be used together with those. */;
 
 
 
 
 
650 D.attr(settingsButton, 'role', 'button');
651 settingsButton.addEventListener('click',function(ev){
652 //ev.preventDefault();
653 if(settingsPopup.isShown()) settingsPopup.hide();
654 else settingsPopup.show(settingsButton);
@@ -665,11 +702,11 @@
665 const rect = settingsButton.getBoundingClientRect();
666 return rect.right - popupSize.width;
667 };
668 settingsPopup.options.adjustY = function(y){
669 const rect = settingsButton.getBoundingClientRect();
670 if(Chat.isChatOnlyMode()){
671 return rect.top - popupSize.height -2;
672 }else{
673 return rect.bottom + 2;
674 }
675 };
676
--- src/chat.js
+++ src/chat.js
@@ -21,11 +21,12 @@
21 inputForm: E1('#chat-form'),
22 btnSubmit: E1('#chat-message-submit'),
23 inputSingle: E1('#chat-input-single'),
24 inputMulti: E1('#chat-input-multi'),
25 inputCurrent: undefined/*one of inputSingle or inputMulti*/,
26 inputFile: E1('#chat-input-file'),
27 contentDiv: E1('div.content')
28 },
29 me: F.user.name,
30 mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
31 mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
32 pageIsActive: 'visible'===document.visibilityState,
@@ -64,10 +65,11 @@
65 this.e.inputCurrent = this.e.inputSingle;
66 }
67 D.addClass(old, 'hidden');
68 D.removeClass(this.e.inputCurrent, 'hidden');
69 this.e.inputCurrent.value = old.value;
70 old.value = '';
71 return this;
72 },
73 /** Enables (if yes is truthy) or disables all elements in
74 * this.disableDuringAjax. */
75 enableAjaxComponents: function(yes){
@@ -115,26 +117,34 @@
117 if(atEnd){
118 mip.parentNode.insertBefore(e, mip);
119 }else{
120 if(mip.nextSibling) mip.parentNode.insertBefore(e, mip.nextSibling);
121 else mip.parentNode.appendChild(e);
122 if(this.isUiFlipped()){
123 /* When UI is flipped, new messages start out under the
124 text input area because of its position:sticky
125 style. We have to scroll them up. When the page footer
126 is not hidden but is not on-screen, this causes a
127 slight amount of UI jarring as the footer is *also*
128 scrolled into view (for whatever reason). */
129 setTimeout(()=>e.scrollIntoView(), 0);
 
 
130 }
131 }
132 },
133 /** Returns true if chat-only mode is enabled. */
134 isChatOnlyMode: ()=>document.body.classList.contains('chat-only-mode'),
135 /** Returns true if the UI seems to be in "bottom-up" mode. */
136 isUiFlipped: function(){
137 const style = window.getComputedStyle(this.e.contentDiv);
138 return style.flexDirection.indexOf("-reverse")>0;
139 },
140 /**
141 Enters (if passed a truthy value or no arguments) or leaves
142 "chat-only" mode. That mode hides the page's header and
143 footer, leaving only the chat application visible to the
144 user.
145 */
146 chatOnlyMode: function f(yes){
147 if(undefined === f.elemsToToggle){
148 f.elemsToToggle = [];
149 document.body.childNodes.forEach(function(e){
150 if(!e.classList) return/*TEXT nodes and such*/;
@@ -153,15 +163,17 @@
163 document.body.scroll(0,document.body.height);
164 }else{
165 D.removeClass(f.elemsToToggle, 'hidden');
166 D.removeClass(document.body, 'chat-only-mode');
167 setTimeout(()=>document.body.scrollIntoView(
168 /*moves to (0,0), whereas scrollTo(0,0) does not!
169 setTimeout() is unfortunately necessary to get the scroll
170 placement correct.*/
171 ), 0);
172 }
173 const msg = document.querySelector('.message-widget');
174 if(msg) setTimeout(()=>msg.scrollIntoView(),0);
175 return this;
176 },
177 toggleChatOnlyMode: function(){
178 return this.chatOnlyMode(!this.isChatOnlyMode());
179 },
@@ -169,25 +181,31 @@
181 get: (k,dflt)=>F.storage.get(k,dflt),
182 getBool: (k,dflt)=>F.storage.getBool(k,dflt),
183 set: (k,v)=>F.storage.set(k,v),
184 defaults:{
185 "images-inline": !!F.config.chat.imagesInline,
186 "monospace-messages": false,
187 "bottom-up": true
188 }
189 }
190 };
191 /* Install default settings... */
192 Object.keys(cs.settings.defaults).forEach(function(k){
193 const v = cs.settings.get(k,cs);
194 if(cs===v) cs.settings.set(k,cs.settings.defaults[k]);
195 });
196 if(window.innerWidth<window.innerHeight){
197 /* Alignment of 'my' messages: right alignment is conventional
198 for mobile chat apps but can be difficult to read in wide
199 windows (desktop/tablet landscape mode), so we default to a
200 layout based on the apparently "orientation" of the window:
201 tall vs wide. Can be toggled via settings popup. */
202 document.body.classList.add('my-messages-right');
203 }
204 if(cs.settings.getBool("bottom-up")){
205 document.body.classList.add('chat-bottom-up');
206 }
207 if(cs.settings.getBool('monospace-messages',false)){
208 document.body.classList.add('monospace-messages');
209 }
210 cs.e.inputCurrent = cs.e.inputSingle;
211 cs.pageTitleOrig = cs.e.pageTitle.innerText;
@@ -327,12 +345,15 @@
345 this.e.content.style.backgroundColor = m.uclr;
346 this.e.tab.style.backgroundColor = m.uclr;
347
348 const d = new Date(m.mtime);
349 D.append(
350 D.clearElement(this.e.tab),
351 D.text(
352 m.xfrom," #",m.msgid,' @ ',d.getHours(),":",
353 (d.getMinutes()+100).toString().slice(1,3)
354 )
355 );
356 var contentTarget = this.e.content;
357 if( m.fsize>0 ){
358 if( m.fmime
359 && m.fmime.startsWith("image/")
@@ -347,14 +368,17 @@
368 "(" + m.fname + " " + m.fsize + " bytes)"
369 )
370 D.attr(a,'target','_blank');
371 contentTarget.appendChild(a);
372 }
373 ;
374 }
375 if(m.xmsg){
376 if(m.fsize>0){
377 /* We have file/image content, so need another element for
378 the message text. */
379 contentTarget = D.div();
380 D.append(this.e.content, contentTarget);
381 }
382 // The m.xmsg text comes from the same server as this script and
383 // is guaranteed by that server to be "safe" HTML - safe in the
384 // sense that it is not possible for a malefactor to inject HTML
@@ -552,11 +576,11 @@
576 Chat.deleteMessage(eMsg);
577 });
578 }
579 }/*refresh()*/
580 });
581 f.popup.installHideHandlers();
582 f.popup.hide = function(){
583 delete this._eMsg;
584 D.clearElement(this.e);
585 return this.show(false);
586 };
@@ -608,10 +632,18 @@
632 label: "Left-align my posts",
633 boolValue: ()=>!document.body.classList.contains('my-messages-right'),
634 callback: function f(){
635 document.body.classList.toggle('my-messages-right');
636 }
637 },{
638 label: "Bottom-up chat",
639 boolValue: ()=>document.body.classList.contains('chat-bottom-up'),
640 callback: function(){
641 document.body.classList.toggle('chat-bottom-up');
642 Chat.settings.set('bottom-up',
643 document.body.classList.contains('chat-bottom-up'));
644 }
645 },{
646 label: "Images inline",
647 boolValue: ()=>Chat.settings.getBool('images-inline'),
648 callback: function(){
649 const v = Chat.settings.getBool('images-inline',true);
@@ -642,13 +674,18 @@
674 }
675 D.append(settingsPopup.e, line);
676 btn.addEventListener('click', callback);
677 });
678 };
679 settingsPopup.installHideHandlers(false, true, true)
680 /** Reminder: click-to-hide interferes with "?" embedded within
681 the popup, so cannot be used together with those. Enabling
682 this means, however, that tapping the menu button to toggle
683 the menu cannot work because tapping the menu button while the
684 menu is opened will, because of the click-to-hide handler,
685 hide the menu before the button gets an event saying to toggle
686 it.*/;
687 D.attr(settingsButton, 'role', 'button');
688 settingsButton.addEventListener('click',function(ev){
689 //ev.preventDefault();
690 if(settingsPopup.isShown()) settingsPopup.hide();
691 else settingsPopup.show(settingsButton);
@@ -665,11 +702,11 @@
702 const rect = settingsButton.getBoundingClientRect();
703 return rect.right - popupSize.width;
704 };
705 settingsPopup.options.adjustY = function(y){
706 const rect = settingsButton.getBoundingClientRect();
707 if(Chat.isUiFlipped()){
708 return rect.top - popupSize.height -2;
709 }else{
710 return rect.bottom + 2;
711 }
712 };
713
+17 -22
--- src/default.css
+++ src/default.css
@@ -1503,24 +1503,21 @@
15031503
body.chat.monospace-messages .message-widget-content,
15041504
body.chat.monospace-messages textarea,
15051505
body.chat.monospace-messages input[type=text]{
15061506
font-family: monospace;
15071507
}
1508
-
15091508
/* User name and timestamp (a LEGEND-like element) */
15101509
body.chat .message-widget .message-widget-tab {
15111510
border-radius: 0.25em 0.25em 0 0;
15121511
padding: 0 0.5em;
15131512
margin: 0 0.25em 0em 0.15em;
15141513
padding: 0 0.5em 0.15em 0.5em;
15151514
cursor: pointer;
15161515
}
1517
-
15181516
body.chat .fossil-tooltip.help-buttonlet-content {
15191517
font-size: 80%;
15201518
}
1521
-
15221519
/* The popup element for displaying message timestamps
15231520
and deletion controls. */
15241521
body.chat .chat-message-popup {
15251522
font-family: monospace;
15261523
font-size: 0.8em;
@@ -1529,10 +1526,11 @@
15291526
flex-direction: column;
15301527
align-items: stretch;
15311528
padding: 0.25em;
15321529
z-index: 200;
15331530
}
1531
+/* Full message timestamps. */
15341532
body.chat .chat-message-popup > span { white-space: nowrap; }
15351533
/* Container for the message deletion buttons. */
15361534
body.chat .chat-message-popup > .toolbar {
15371535
padding: 0.2em;
15381536
margin: 0;
@@ -1544,19 +1542,18 @@
15441542
flex-wrap: wrap;
15451543
}
15461544
body.chat .chat-message-popup > .toolbar > button {
15471545
flex: 1 1 auto;
15481546
}
1549
-
1550
-/* The main widget for loading more/older chat messages. */
1547
+/* The widget for loading more/older chat messages. */
15511548
body.chat #load-msg-toolbar {
15521549
border-radius: 0.25em;
15531550
padding: 0.1em 0.2em;
15541551
margin-bottom: 1em;
15551552
}
1556
-/* Set when chat has loaded all of the available historical
1557
- messages */
1553
+/* .all-done is set when chat has loaded all of the available
1554
+ historical messages */
15581555
body.chat #load-msg-toolbar.all-done {
15591556
opacity: 0.5;
15601557
}
15611558
body.chat #load-msg-toolbar > div {
15621559
display: flex;
@@ -1600,14 +1597,12 @@
16001597
body.fossil-dark-style .settings-icon {
16011598
filter: invert(100%);
16021599
}
16031600
/* "Chat-only mode" hides the site header/footer, showing only
16041601
the chat app. */
1605
-body.chat.chat-only-mode{
1606
-}
1607
-body.chat #chat-settings-button {
1608
-}
1602
+body.chat.chat-only-mode{}
1603
+body.chat #chat-settings-button {}
16091604
/** Popup widget for the /chat settings. */
16101605
body.chat .chat-settings-popup {
16111606
font-size: 0.8em;
16121607
text-align: left;
16131608
display: flex;
@@ -1644,50 +1639,48 @@
16441639
/** Container for the list of /chat messages. */
16451640
body.chat #chat-messages-wrapper {
16461641
display: flex;
16471642
flex-direction: column;
16481643
}
1649
-body.chat.chat-only-mode #chat-messages-wrapper {
1644
+body.chat.chat-bottom-up #chat-messages-wrapper {
16501645
flex-direction: column-reverse;
1651
- position: relative;
1652
- top: 0;
16531646
z-index: 99 /* so that it scrolls under input area. If it's
16541647
lower than div.content then mouse events to it
16551648
are blocked!*/;
16561649
}
1657
-body.chat.chat-only-mode > div.content {
1650
+body.chat > div.content {
16581651
margin: 0;
16591652
padding: 0;
16601653
display: flex;
16611654
flex-direction: column;
16621655
align-items: stretch;
16631656
}
1664
-body.chat.chat-only-mode > div.content {
1657
+body.chat.chat-bottom-up > div.content {
16651658
flex-direction: column-reverse;
16661659
}
16671660
/* Wrapper for /chat user input controls */
16681661
body.chat #chat-input-area {
16691662
display: flex;
16701663
flex-direction: column;
16711664
border-bottom: 1px solid black;
16721665
padding: 0.5em 1em;
16731666
margin-bottom: 0.5em;
1674
- /*position: sticky; top: 0;*/
1675
- /*position: -webkit-sticky*/ /* supposedly some versions of Safari */;
1676
-}
1677
-body.chat.chat-only-mode #chat-input-area {
1667
+ position: sticky; top: 0;
16781668
z-index: 100
16791669
/* see notes in #chat-messages-wrapper. The various popups require a
16801670
z-index higher than this one. */;
1671
+}
1672
+body.chat.chat-bottom-up #chat-input-area {
16811673
border-bottom: none;
16821674
border-top: 1px solid black;
16831675
margin-bottom: 0;
16841676
margin-top: 0.5em;
16851677
position: sticky;
1686
- position: -webkit-sticky/* supposedly some versions of Safari */;
16871678
bottom: 0;
16881679
}
1680
+/* Widget holding the chat message input field, send button, and
1681
+ settings button. */
16891682
body.chat #chat-input-line {
16901683
display: flex;
16911684
flex-direction: row;
16921685
margin-bottom: 0.25em;
16931686
align-items: flex-start;
@@ -1699,10 +1692,11 @@
16991692
}
17001693
body.chat #chat-input-line > input[type=text],
17011694
body.chat #chat-input-line > textarea {
17021695
flex: 5 1 auto;
17031696
}
1697
+/* Widget holding the file selection control and preview */
17041698
body.chat #chat-input-file-area {
17051699
display: flex;
17061700
flex-direction: row;
17071701
align-items: center;
17081702
flex-wrap: wrap;
@@ -1723,16 +1717,17 @@
17231717
padding: 0.25em;
17241718
}
17251719
body.chat #chat-input-file > input {
17261720
flex: 1 0 auto;
17271721
}
1722
+/* Indicator when a drag/drop is in progress */
17281723
body.chat #chat-input-file.dragover {
17291724
border: 1px dashed green;
17301725
}
1726
+/* Widget holding the details of a selected/dropped file/image. */
17311727
body.chat #chat-drop-details {
17321728
flex: 0 1 auto;
17331729
padding: 0.5em 1em;
17341730
margin-left: 0.5em;
17351731
white-space: pre;
17361732
font-family: monospace;
1737
- max-width: 50%;
17381733
}
17391734
--- src/default.css
+++ src/default.css
@@ -1503,24 +1503,21 @@
1503 body.chat.monospace-messages .message-widget-content,
1504 body.chat.monospace-messages textarea,
1505 body.chat.monospace-messages input[type=text]{
1506 font-family: monospace;
1507 }
1508
1509 /* User name and timestamp (a LEGEND-like element) */
1510 body.chat .message-widget .message-widget-tab {
1511 border-radius: 0.25em 0.25em 0 0;
1512 padding: 0 0.5em;
1513 margin: 0 0.25em 0em 0.15em;
1514 padding: 0 0.5em 0.15em 0.5em;
1515 cursor: pointer;
1516 }
1517
1518 body.chat .fossil-tooltip.help-buttonlet-content {
1519 font-size: 80%;
1520 }
1521
1522 /* The popup element for displaying message timestamps
1523 and deletion controls. */
1524 body.chat .chat-message-popup {
1525 font-family: monospace;
1526 font-size: 0.8em;
@@ -1529,10 +1526,11 @@
1529 flex-direction: column;
1530 align-items: stretch;
1531 padding: 0.25em;
1532 z-index: 200;
1533 }
 
1534 body.chat .chat-message-popup > span { white-space: nowrap; }
1535 /* Container for the message deletion buttons. */
1536 body.chat .chat-message-popup > .toolbar {
1537 padding: 0.2em;
1538 margin: 0;
@@ -1544,19 +1542,18 @@
1544 flex-wrap: wrap;
1545 }
1546 body.chat .chat-message-popup > .toolbar > button {
1547 flex: 1 1 auto;
1548 }
1549
1550 /* The main widget for loading more/older chat messages. */
1551 body.chat #load-msg-toolbar {
1552 border-radius: 0.25em;
1553 padding: 0.1em 0.2em;
1554 margin-bottom: 1em;
1555 }
1556 /* Set when chat has loaded all of the available historical
1557 messages */
1558 body.chat #load-msg-toolbar.all-done {
1559 opacity: 0.5;
1560 }
1561 body.chat #load-msg-toolbar > div {
1562 display: flex;
@@ -1600,14 +1597,12 @@
1600 body.fossil-dark-style .settings-icon {
1601 filter: invert(100%);
1602 }
1603 /* "Chat-only mode" hides the site header/footer, showing only
1604 the chat app. */
1605 body.chat.chat-only-mode{
1606 }
1607 body.chat #chat-settings-button {
1608 }
1609 /** Popup widget for the /chat settings. */
1610 body.chat .chat-settings-popup {
1611 font-size: 0.8em;
1612 text-align: left;
1613 display: flex;
@@ -1644,50 +1639,48 @@
1644 /** Container for the list of /chat messages. */
1645 body.chat #chat-messages-wrapper {
1646 display: flex;
1647 flex-direction: column;
1648 }
1649 body.chat.chat-only-mode #chat-messages-wrapper {
1650 flex-direction: column-reverse;
1651 position: relative;
1652 top: 0;
1653 z-index: 99 /* so that it scrolls under input area. If it's
1654 lower than div.content then mouse events to it
1655 are blocked!*/;
1656 }
1657 body.chat.chat-only-mode > div.content {
1658 margin: 0;
1659 padding: 0;
1660 display: flex;
1661 flex-direction: column;
1662 align-items: stretch;
1663 }
1664 body.chat.chat-only-mode > div.content {
1665 flex-direction: column-reverse;
1666 }
1667 /* Wrapper for /chat user input controls */
1668 body.chat #chat-input-area {
1669 display: flex;
1670 flex-direction: column;
1671 border-bottom: 1px solid black;
1672 padding: 0.5em 1em;
1673 margin-bottom: 0.5em;
1674 /*position: sticky; top: 0;*/
1675 /*position: -webkit-sticky*/ /* supposedly some versions of Safari */;
1676 }
1677 body.chat.chat-only-mode #chat-input-area {
1678 z-index: 100
1679 /* see notes in #chat-messages-wrapper. The various popups require a
1680 z-index higher than this one. */;
 
 
1681 border-bottom: none;
1682 border-top: 1px solid black;
1683 margin-bottom: 0;
1684 margin-top: 0.5em;
1685 position: sticky;
1686 position: -webkit-sticky/* supposedly some versions of Safari */;
1687 bottom: 0;
1688 }
 
 
1689 body.chat #chat-input-line {
1690 display: flex;
1691 flex-direction: row;
1692 margin-bottom: 0.25em;
1693 align-items: flex-start;
@@ -1699,10 +1692,11 @@
1699 }
1700 body.chat #chat-input-line > input[type=text],
1701 body.chat #chat-input-line > textarea {
1702 flex: 5 1 auto;
1703 }
 
1704 body.chat #chat-input-file-area {
1705 display: flex;
1706 flex-direction: row;
1707 align-items: center;
1708 flex-wrap: wrap;
@@ -1723,16 +1717,17 @@
1723 padding: 0.25em;
1724 }
1725 body.chat #chat-input-file > input {
1726 flex: 1 0 auto;
1727 }
 
1728 body.chat #chat-input-file.dragover {
1729 border: 1px dashed green;
1730 }
 
1731 body.chat #chat-drop-details {
1732 flex: 0 1 auto;
1733 padding: 0.5em 1em;
1734 margin-left: 0.5em;
1735 white-space: pre;
1736 font-family: monospace;
1737 max-width: 50%;
1738 }
1739
--- src/default.css
+++ src/default.css
@@ -1503,24 +1503,21 @@
1503 body.chat.monospace-messages .message-widget-content,
1504 body.chat.monospace-messages textarea,
1505 body.chat.monospace-messages input[type=text]{
1506 font-family: monospace;
1507 }
 
1508 /* User name and timestamp (a LEGEND-like element) */
1509 body.chat .message-widget .message-widget-tab {
1510 border-radius: 0.25em 0.25em 0 0;
1511 padding: 0 0.5em;
1512 margin: 0 0.25em 0em 0.15em;
1513 padding: 0 0.5em 0.15em 0.5em;
1514 cursor: pointer;
1515 }
 
1516 body.chat .fossil-tooltip.help-buttonlet-content {
1517 font-size: 80%;
1518 }
 
1519 /* The popup element for displaying message timestamps
1520 and deletion controls. */
1521 body.chat .chat-message-popup {
1522 font-family: monospace;
1523 font-size: 0.8em;
@@ -1529,10 +1526,11 @@
1526 flex-direction: column;
1527 align-items: stretch;
1528 padding: 0.25em;
1529 z-index: 200;
1530 }
1531 /* Full message timestamps. */
1532 body.chat .chat-message-popup > span { white-space: nowrap; }
1533 /* Container for the message deletion buttons. */
1534 body.chat .chat-message-popup > .toolbar {
1535 padding: 0.2em;
1536 margin: 0;
@@ -1544,19 +1542,18 @@
1542 flex-wrap: wrap;
1543 }
1544 body.chat .chat-message-popup > .toolbar > button {
1545 flex: 1 1 auto;
1546 }
1547 /* The widget for loading more/older chat messages. */
 
1548 body.chat #load-msg-toolbar {
1549 border-radius: 0.25em;
1550 padding: 0.1em 0.2em;
1551 margin-bottom: 1em;
1552 }
1553 /* .all-done is set when chat has loaded all of the available
1554 historical messages */
1555 body.chat #load-msg-toolbar.all-done {
1556 opacity: 0.5;
1557 }
1558 body.chat #load-msg-toolbar > div {
1559 display: flex;
@@ -1600,14 +1597,12 @@
1597 body.fossil-dark-style .settings-icon {
1598 filter: invert(100%);
1599 }
1600 /* "Chat-only mode" hides the site header/footer, showing only
1601 the chat app. */
1602 body.chat.chat-only-mode{}
1603 body.chat #chat-settings-button {}
 
 
1604 /** Popup widget for the /chat settings. */
1605 body.chat .chat-settings-popup {
1606 font-size: 0.8em;
1607 text-align: left;
1608 display: flex;
@@ -1644,50 +1639,48 @@
1639 /** Container for the list of /chat messages. */
1640 body.chat #chat-messages-wrapper {
1641 display: flex;
1642 flex-direction: column;
1643 }
1644 body.chat.chat-bottom-up #chat-messages-wrapper {
1645 flex-direction: column-reverse;
 
 
1646 z-index: 99 /* so that it scrolls under input area. If it's
1647 lower than div.content then mouse events to it
1648 are blocked!*/;
1649 }
1650 body.chat > div.content {
1651 margin: 0;
1652 padding: 0;
1653 display: flex;
1654 flex-direction: column;
1655 align-items: stretch;
1656 }
1657 body.chat.chat-bottom-up > div.content {
1658 flex-direction: column-reverse;
1659 }
1660 /* Wrapper for /chat user input controls */
1661 body.chat #chat-input-area {
1662 display: flex;
1663 flex-direction: column;
1664 border-bottom: 1px solid black;
1665 padding: 0.5em 1em;
1666 margin-bottom: 0.5em;
1667 position: sticky; top: 0;
 
 
 
1668 z-index: 100
1669 /* see notes in #chat-messages-wrapper. The various popups require a
1670 z-index higher than this one. */;
1671 }
1672 body.chat.chat-bottom-up #chat-input-area {
1673 border-bottom: none;
1674 border-top: 1px solid black;
1675 margin-bottom: 0;
1676 margin-top: 0.5em;
1677 position: sticky;
 
1678 bottom: 0;
1679 }
1680 /* Widget holding the chat message input field, send button, and
1681 settings button. */
1682 body.chat #chat-input-line {
1683 display: flex;
1684 flex-direction: row;
1685 margin-bottom: 0.25em;
1686 align-items: flex-start;
@@ -1699,10 +1692,11 @@
1692 }
1693 body.chat #chat-input-line > input[type=text],
1694 body.chat #chat-input-line > textarea {
1695 flex: 5 1 auto;
1696 }
1697 /* Widget holding the file selection control and preview */
1698 body.chat #chat-input-file-area {
1699 display: flex;
1700 flex-direction: row;
1701 align-items: center;
1702 flex-wrap: wrap;
@@ -1723,16 +1717,17 @@
1717 padding: 0.25em;
1718 }
1719 body.chat #chat-input-file > input {
1720 flex: 1 0 auto;
1721 }
1722 /* Indicator when a drag/drop is in progress */
1723 body.chat #chat-input-file.dragover {
1724 border: 1px dashed green;
1725 }
1726 /* Widget holding the details of a selected/dropped file/image. */
1727 body.chat #chat-drop-details {
1728 flex: 0 1 auto;
1729 padding: 0.5em 1em;
1730 margin-left: 0.5em;
1731 white-space: pre;
1732 font-family: monospace;
 
1733 }
1734
--- src/fossil.dom.js
+++ src/fossil.dom.js
@@ -112,11 +112,15 @@
112112
if(label) e.appendChild(dom.text(true===label ? href : label));
113113
return e;
114114
};
115115
dom.hr = dom.createElemFactory('hr');
116116
dom.br = dom.createElemFactory('br');
117
- dom.text = (t)=>document.createTextNode(t||'');
117
+ /** Returns a new TEXT node which contains the text of all of the
118
+ arguments appended together. */
119
+ dom.text = function(/*...*/){
120
+ return document.createTextNode(argsToArray(arguments).join(''));
121
+ };
118122
dom.button = function(label){
119123
const b = this.create('button');
120124
if(label) b.appendChild(this.text(label));
121125
return b;
122126
};
123127
--- src/fossil.dom.js
+++ src/fossil.dom.js
@@ -112,11 +112,15 @@
112 if(label) e.appendChild(dom.text(true===label ? href : label));
113 return e;
114 };
115 dom.hr = dom.createElemFactory('hr');
116 dom.br = dom.createElemFactory('br');
117 dom.text = (t)=>document.createTextNode(t||'');
 
 
 
 
118 dom.button = function(label){
119 const b = this.create('button');
120 if(label) b.appendChild(this.text(label));
121 return b;
122 };
123
--- src/fossil.dom.js
+++ src/fossil.dom.js
@@ -112,11 +112,15 @@
112 if(label) e.appendChild(dom.text(true===label ? href : label));
113 return e;
114 };
115 dom.hr = dom.createElemFactory('hr');
116 dom.br = dom.createElemFactory('br');
117 /** Returns a new TEXT node which contains the text of all of the
118 arguments appended together. */
119 dom.text = function(/*...*/){
120 return document.createTextNode(argsToArray(arguments).join(''));
121 };
122 dom.button = function(label){
123 const b = this.create('button');
124 if(label) b.appendChild(this.text(label));
125 return b;
126 };
127
--- src/fossil.popupwidget.js
+++ src/fossil.popupwidget.js
@@ -217,18 +217,32 @@
217217
/**
218218
A convenience method which adds click handlers to this popup's
219219
main element and document.body to hide (via hide()) the popup
220220
when either element is clicked or the ESC key is pressed. Only
221221
call this once per instance, if at all. Returns this;
222
+
223
+ The first argument specifies whether a click handler on this
224
+ object is installed. The second specifies whether a click
225
+ outside of this object should close it. The third specifies
226
+ whether an ESC handler is installed.
227
+
228
+ Passing no arguments is equivalent to passing (true,true,true),
229
+ and passing fewer arguments defaults the unpassed parameters to
230
+ true.
222231
*/
223
- installClickToHide: function f(){
224
- this.e.addEventListener('click', ()=>this.hide(), false);
225
- document.body.addEventListener('click', ()=>this.hide(), true);
226
- const self = this;
227
- document.body.addEventListener('keydown', function(ev){
228
- if(self.isShown() && 27===ev.which) self.hide();
229
- }, true);
232
+ installHideHandlers: function f(onClickSelf, onClickOther, onEsc){
233
+ if(!arguments.length) onClick = onClickOther = onEsc = true;
234
+ else if(2===arguments.length) onClickOther = onEsc = true;
235
+ else if(1===arguments.length) onEsc = true;
236
+ if(onClickSelf) this.e.addEventListener('click', ()=>this.hide(), false);
237
+ if(onClickOther) document.body.addEventListener('click', ()=>this.hide(), true);
238
+ if(onEsc){
239
+ const self = this;
240
+ document.body.addEventListener('keydown', function(ev){
241
+ if(self.isShown() && 27===ev.which) self.hide();
242
+ }, true);
243
+ }
230244
return this;
231245
}
232246
}/*F.PopupWidget.prototype*/;
233247
234248
/**
@@ -342,11 +356,11 @@
342356
cssClass: ['fossil-tooltip', 'help-buttonlet-content'],
343357
refresh: function(){
344358
}
345359
});
346360
fch.popup.e.style.maxWidth = '80%'/*of body*/;
347
- fch.popup.installClickToHide();
361
+ fch.popup.installHideHandlers();
348362
}
349363
D.append(D.clearElement(fch.popup.e), ev.target.$helpContent);
350364
/* Shift the help around a bit to "better" fit the
351365
screen. However, fch.popup.e.getClientRects() is empty
352366
until the popup is shown, so we have to show it,
353367
--- src/fossil.popupwidget.js
+++ src/fossil.popupwidget.js
@@ -217,18 +217,32 @@
217 /**
218 A convenience method which adds click handlers to this popup's
219 main element and document.body to hide (via hide()) the popup
220 when either element is clicked or the ESC key is pressed. Only
221 call this once per instance, if at all. Returns this;
 
 
 
 
 
 
 
 
 
222 */
223 installClickToHide: function f(){
224 this.e.addEventListener('click', ()=>this.hide(), false);
225 document.body.addEventListener('click', ()=>this.hide(), true);
226 const self = this;
227 document.body.addEventListener('keydown', function(ev){
228 if(self.isShown() && 27===ev.which) self.hide();
229 }, true);
 
 
 
 
 
230 return this;
231 }
232 }/*F.PopupWidget.prototype*/;
233
234 /**
@@ -342,11 +356,11 @@
342 cssClass: ['fossil-tooltip', 'help-buttonlet-content'],
343 refresh: function(){
344 }
345 });
346 fch.popup.e.style.maxWidth = '80%'/*of body*/;
347 fch.popup.installClickToHide();
348 }
349 D.append(D.clearElement(fch.popup.e), ev.target.$helpContent);
350 /* Shift the help around a bit to "better" fit the
351 screen. However, fch.popup.e.getClientRects() is empty
352 until the popup is shown, so we have to show it,
353
--- src/fossil.popupwidget.js
+++ src/fossil.popupwidget.js
@@ -217,18 +217,32 @@
217 /**
218 A convenience method which adds click handlers to this popup's
219 main element and document.body to hide (via hide()) the popup
220 when either element is clicked or the ESC key is pressed. Only
221 call this once per instance, if at all. Returns this;
222
223 The first argument specifies whether a click handler on this
224 object is installed. The second specifies whether a click
225 outside of this object should close it. The third specifies
226 whether an ESC handler is installed.
227
228 Passing no arguments is equivalent to passing (true,true,true),
229 and passing fewer arguments defaults the unpassed parameters to
230 true.
231 */
232 installHideHandlers: function f(onClickSelf, onClickOther, onEsc){
233 if(!arguments.length) onClick = onClickOther = onEsc = true;
234 else if(2===arguments.length) onClickOther = onEsc = true;
235 else if(1===arguments.length) onEsc = true;
236 if(onClickSelf) this.e.addEventListener('click', ()=>this.hide(), false);
237 if(onClickOther) document.body.addEventListener('click', ()=>this.hide(), true);
238 if(onEsc){
239 const self = this;
240 document.body.addEventListener('keydown', function(ev){
241 if(self.isShown() && 27===ev.which) self.hide();
242 }, true);
243 }
244 return this;
245 }
246 }/*F.PopupWidget.prototype*/;
247
248 /**
@@ -342,11 +356,11 @@
356 cssClass: ['fossil-tooltip', 'help-buttonlet-content'],
357 refresh: function(){
358 }
359 });
360 fch.popup.e.style.maxWidth = '80%'/*of body*/;
361 fch.popup.installHideHandlers();
362 }
363 D.append(D.clearElement(fch.popup.e), ev.target.$helpContent);
364 /* Shift the help around a bit to "better" fit the
365 screen. However, fch.popup.e.getClientRects() is empty
366 until the popup is shown, so we have to show it,
367

Keyboard Shortcuts

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