Fossil SCM

Bottom-up chat mode.

drh 2020-12-26 16:24 trunk merge
Commit 2572872bb0de2179f92bcb7b34b397e8995dcb6a8ce6272315a1fa47de470923
+3 -1
--- src/chat.c
+++ src/chat.c
@@ -523,11 +523,13 @@
523523
/*
524524
** WEBPAGE: chat-download
525525
**
526526
** Download the CHAT.FILE attachment associated with a single chat
527527
** entry. The "name" query parameter begins with an integer that
528
-** identifies the particular chat message.
528
+** identifies the particular chat message. The integer may be followed
529
+** by a / and a filename, which will indicate to the browser to use
530
+** the indicated name when saving the file.
529531
*/
530532
void chat_download_webpage(void){
531533
int msgid;
532534
Blob r;
533535
const char *zMime;
534536
--- src/chat.c
+++ src/chat.c
@@ -523,11 +523,13 @@
523 /*
524 ** WEBPAGE: chat-download
525 **
526 ** Download the CHAT.FILE attachment associated with a single chat
527 ** entry. The "name" query parameter begins with an integer that
528 ** identifies the particular chat message.
 
 
529 */
530 void chat_download_webpage(void){
531 int msgid;
532 Blob r;
533 const char *zMime;
534
--- src/chat.c
+++ src/chat.c
@@ -523,11 +523,13 @@
523 /*
524 ** WEBPAGE: chat-download
525 **
526 ** Download the CHAT.FILE attachment associated with a single chat
527 ** entry. The "name" query parameter begins with an integer that
528 ** identifies the particular chat message. The integer may be followed
529 ** by a / and a filename, which will indicate to the browser to use
530 ** the indicated name when saving the file.
531 */
532 void chat_download_webpage(void){
533 int msgid;
534 Blob r;
535 const char *zMime;
536
+3 -1
--- src/chat.c
+++ src/chat.c
@@ -523,11 +523,13 @@
523523
/*
524524
** WEBPAGE: chat-download
525525
**
526526
** Download the CHAT.FILE attachment associated with a single chat
527527
** entry. The "name" query parameter begins with an integer that
528
-** identifies the particular chat message.
528
+** identifies the particular chat message. The integer may be followed
529
+** by a / and a filename, which will indicate to the browser to use
530
+** the indicated name when saving the file.
529531
*/
530532
void chat_download_webpage(void){
531533
int msgid;
532534
Blob r;
533535
const char *zMime;
534536
--- src/chat.c
+++ src/chat.c
@@ -523,11 +523,13 @@
523 /*
524 ** WEBPAGE: chat-download
525 **
526 ** Download the CHAT.FILE attachment associated with a single chat
527 ** entry. The "name" query parameter begins with an integer that
528 ** identifies the particular chat message.
 
 
529 */
530 void chat_download_webpage(void){
531 int msgid;
532 Blob r;
533 const char *zMime;
534
--- src/chat.c
+++ src/chat.c
@@ -523,11 +523,13 @@
523 /*
524 ** WEBPAGE: chat-download
525 **
526 ** Download the CHAT.FILE attachment associated with a single chat
527 ** entry. The "name" query parameter begins with an integer that
528 ** identifies the particular chat message. The integer may be followed
529 ** by a / and a filename, which will indicate to the browser to use
530 ** the indicated name when saving the file.
531 */
532 void chat_download_webpage(void){
533 int msgid;
534 Blob r;
535 const char *zMime;
536
+158 -69
--- src/chat.js
+++ src/chat.js
@@ -7,24 +7,27 @@
77
const E1 = function(selector){
88
const e = document.querySelector(selector);
99
if(!e) throw new Error("missing required DOM element: "+selector);
1010
return e;
1111
};
12
+ //document.body.classList.add('chat-only-mode');
1213
const Chat = (function(){
1314
const cs = {
1415
e:{/*map of certain DOM elements.*/
1516
messageInjectPoint: E1('#message-inject-point'),
1617
pageTitle: E1('head title'),
1718
loadToolbar: undefined /* the load-posts toolbar (dynamically created) */,
1819
inputWrapper: E1("#chat-input-area"),
20
+ fileSelectWrapper: E1('#chat-input-file-area'),
1921
messagesWrapper: E1('#chat-messages-wrapper'),
2022
inputForm: E1('#chat-form'),
2123
btnSubmit: E1('#chat-message-submit'),
2224
inputSingle: E1('#chat-input-single'),
2325
inputMulti: E1('#chat-input-multi'),
2426
inputCurrent: undefined/*one of inputSingle or inputMulti*/,
25
- inputFile: E1('#chat-input-file')
27
+ inputFile: E1('#chat-input-file'),
28
+ contentDiv: E1('div.content')
2629
},
2730
me: F.user.name,
2831
mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
2932
mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
3033
pageIsActive: 'visible'===document.visibilityState,
@@ -63,10 +66,11 @@
6366
this.e.inputCurrent = this.e.inputSingle;
6467
}
6568
D.addClass(old, 'hidden');
6669
D.removeClass(this.e.inputCurrent, 'hidden');
6770
this.e.inputCurrent.value = old.value;
71
+ old.value = '';
6872
return this;
6973
},
7074
/** Enables (if yes is truthy) or disables all elements in
7175
* this.disableDuringAjax. */
7276
enableAjaxComponents: function(yes){
@@ -112,43 +116,141 @@
112116
injectMessageElem: function f(e, atEnd){
113117
const mip = atEnd ? this.e.loadToolbar : this.e.messageInjectPoint;
114118
if(atEnd){
115119
mip.parentNode.insertBefore(e, mip);
116120
}else{
117
- if(mip.nextSibling){
118
- mip.parentNode.insertBefore(e, mip.nextSibling);
119
- }else{
120
- mip.parentNode.appendChild(e);
121
+ const self = this;
122
+ if(false && 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
+
130
+ The remaining problem here is messages with IMG tags.
131
+ At this point in the process their IMG.src has not yet
132
+ been loaded - that's async. We scroll the message into
133
+ view, but then the downstream loading of IMG.src pushes
134
+ the message content back down, sliding the message
135
+ behind the input field. This can be verified by delaying the
136
+ message scroll by a second or so to give the image time
137
+ to load (from a local server instance).
138
+ */
139
+ D.addClass(self.e.inputWrapper,'unsticky');
140
+ }
141
+ if(mip.nextSibling) mip.parentNode.insertBefore(e, mip.nextSibling);
142
+ else mip.parentNode.appendChild(e);
143
+ if(false && this.isUiFlipped()){
144
+ //e.scrollIntoView();
145
+ setTimeout(function(){
146
+ //self.e.inputWrapper.scrollIntoView();
147
+ //self.e.fileSelectWrapper.scrollIntoView();
148
+ //e.scrollIntoView();
149
+ //D.removeClass(self.e.inputWrapper,'unsticky');
150
+ self.e.inputWrapper.scrollIntoView();
151
+ },0);
121152
}
122153
}
123154
},
155
+ /** Returns true if chat-only mode is enabled. */
156
+ isChatOnlyMode: ()=>document.body.classList.contains('chat-only-mode'),
157
+ /** Returns true if the UI seems to be in "bottom-up" mode. */
158
+ isUiFlipped: function(){
159
+ const style = window.getComputedStyle(this.e.contentDiv);
160
+ return style.flexDirection.indexOf("-reverse")>0;
161
+ },
162
+ /**
163
+ Enters (if passed a truthy value or no arguments) or leaves
164
+ "chat-only" mode. That mode hides the page's header and
165
+ footer, leaving only the chat application visible to the
166
+ user.
167
+ */
168
+ chatOnlyMode: function f(yes){
169
+ if(undefined === f.elemsToToggle){
170
+ f.elemsToToggle = [];
171
+ document.body.childNodes.forEach(function(e){
172
+ if(!e.classList) return/*TEXT nodes and such*/;
173
+ else if(!e.classList.contains('content')
174
+ && !e.classList.contains('fossil-PopupWidget')
175
+ /*kludge^^^ for settingsPopup click handling!*/){
176
+ f.elemsToToggle.push(e);
177
+ }
178
+ });
179
+ }
180
+ if(!arguments.length) yes = true;
181
+ if(yes === this.isChatOnlyMode()) return this;
182
+ if(yes){
183
+ D.addClass(f.elemsToToggle, 'hidden');
184
+ D.addClass(document.body, 'chat-only-mode');
185
+ document.body.scroll(0,document.body.height);
186
+ }else{
187
+ D.removeClass(f.elemsToToggle, 'hidden');
188
+ D.removeClass(document.body, 'chat-only-mode');
189
+ setTimeout(()=>document.body.scrollIntoView(
190
+ /*moves to (0,0), whereas scrollTo(0,0) does not!
191
+ setTimeout() is unfortunately necessary to get the scroll
192
+ placement correct.*/
193
+ ), 0);
194
+ }
195
+ const msg = document.querySelector('.message-widget');
196
+ if(msg) setTimeout(()=>msg.scrollIntoView(),0);
197
+ return this;
198
+ },
199
+ toggleChatOnlyMode: function(){
200
+ return this.chatOnlyMode(!this.isChatOnlyMode());
201
+ },
124202
settings:{
125203
get: (k,dflt)=>F.storage.get(k,dflt),
126204
getBool: (k,dflt)=>F.storage.getBool(k,dflt),
127205
set: (k,v)=>F.storage.set(k,v),
128206
defaults:{
129207
"images-inline": !!F.config.chat.imagesInline,
130
- "monospace-messages": false
208
+ "monospace-messages": false,
209
+ "bottom-up": true
131210
}
132211
}
133212
};
134
- Object.keys(cs.settings.defaults).forEach(function f(k){
135
- const v = cs.settings.get(k,f);
136
- if(f===v) cs.settings.set(k,cs.settings.defaults[k]);
213
+ /* Install default settings... */
214
+ Object.keys(cs.settings.defaults).forEach(function(k){
215
+ const v = cs.settings.get(k,cs);
216
+ if(cs===v) cs.settings.set(k,cs.settings.defaults[k]);
137217
});
138218
if(window.innerWidth<window.innerHeight){
139219
/* Alignment of 'my' messages: right alignment is conventional
140220
for mobile chat apps but can be difficult to read in wide
141
- windows (desktop/tablet landscape mode). Can be toggled via
142
- settings popup. */
221
+ windows (desktop/tablet landscape mode), so we default to a
222
+ layout based on the apparently "orientation" of the window:
223
+ tall vs wide. Can be toggled via settings popup. */
143224
document.body.classList.add('my-messages-right');
144225
}
226
+ if(cs.settings.getBool("bottom-up")){
227
+ document.body.classList.add('chat-bottom-up');
228
+ }
145229
if(cs.settings.getBool('monospace-messages',false)){
146230
document.body.classList.add('monospace-messages');
147231
}
148232
cs.e.inputCurrent = cs.e.inputSingle;
149233
cs.pageTitleOrig = cs.e.pageTitle.innerText;
234
+
235
+ if(true){
236
+ /* In order to make the input area opaque, such that the message
237
+ list scrolls under it without being visible, we have to
238
+ ensure that the input area has a non-transparent background
239
+ color. Ideally we'd select the color of div.content, but that
240
+ is not necessarily set, so we fall back to using the body's
241
+ background color and hope it's been explicitly set
242
+ somewhere. If we rely on the input area having its own color
243
+ specified in CSS then all skins would have to define an
244
+ appropriate color. Thus our selection of the body color,
245
+ while slightly unfortunate, is in the interest of keeping
246
+ skins from being forced to define an opaque bg color.
247
+ */
248
+ const bodyStyle = window.getComputedStyle(document.body);
249
+ cs.e.inputWrapper.style.backgroundColor = bodyStyle.backgroundColor;
250
+ }
251
+
150252
const qs = (e)=>document.querySelector(e);
151253
const argsToArray = function(args){
152254
return Array.prototype.slice.call(args,0);
153255
};
154256
cs.reportError = function(/*msg args*/){
@@ -265,12 +367,15 @@
265367
this.e.content.style.backgroundColor = m.uclr;
266368
this.e.tab.style.backgroundColor = m.uclr;
267369
268370
const d = new Date(m.mtime);
269371
D.append(
270
- D.clearElement(this.e.tab), D.text(
271
- m.xfrom+' @ '+d.getHours()+":"+(d.getMinutes()+100).toString().slice(1,3))
372
+ D.clearElement(this.e.tab),
373
+ D.text(
374
+ m.xfrom," #",m.msgid,' @ ',d.getHours(),":",
375
+ (d.getMinutes()+100).toString().slice(1,3)
376
+ )
272377
);
273378
var contentTarget = this.e.content;
274379
if( m.fsize>0 ){
275380
if( m.fmime
276381
&& m.fmime.startsWith("image/")
@@ -285,14 +390,17 @@
285390
"(" + m.fname + " " + m.fsize + " bytes)"
286391
)
287392
D.attr(a,'target','_blank');
288393
contentTarget.appendChild(a);
289394
}
290
- contentTarget = D.div();
395
+ ;
291396
}
292397
if(m.xmsg){
293
- if(contentTarget !== this.e.content){
398
+ if(m.fsize>0){
399
+ /* We have file/image content, so need another element for
400
+ the message text. */
401
+ contentTarget = D.div();
294402
D.append(this.e.content, contentTarget);
295403
}
296404
// The m.xmsg text comes from the same server as this script and
297405
// is guaranteed by that server to be "safe" HTML - safe in the
298406
// sense that it is not possible for a malefactor to inject HTML
@@ -361,11 +469,11 @@
361469
}
362470
}, false);
363471
/* Add help button for drag/drop/paste zone */
364472
Chat.e.inputFile.parentNode.insertBefore(
365473
F.helpButtonlets.create(
366
- document.querySelector('#chat-input-file-area .help-buttonlet')
474
+ Chat.e.fileSelectWrapper.querySelector('.help-buttonlet')
367475
), Chat.e.inputFile
368476
);
369477
////////////////////////////////////////////////////////////
370478
// File drag/drop visual notification.
371479
const dropHighlight = Chat.e.inputFile /* target zone */;
@@ -490,11 +598,11 @@
490598
Chat.deleteMessage(eMsg);
491599
});
492600
}
493601
}/*refresh()*/
494602
});
495
- f.popup.installClickToHide();
603
+ f.popup.installHideHandlers();
496604
f.popup.hide = function(){
497605
delete this._eMsg;
498606
D.clearElement(this.e);
499607
return this.show(false);
500608
};
@@ -517,15 +625,11 @@
517625
518626
(function(){/*Set up #chat-settings-button */
519627
const settingsButton = document.querySelector('#chat-settings-button');
520628
var popupSize = undefined/*placement workaround*/;
521629
const settingsPopup = new F.PopupWidget({
522
- cssClass: ['fossil-tooltip', 'chat-settings-popup'],
523
- adjustY: function(y){
524
- const rect = settingsButton.getBoundingClientRect();
525
- return rect.top + rect.height + 2;
526
- }
630
+ cssClass: ['fossil-tooltip', 'chat-settings-popup']
527631
});
528632
/* Settings menu entries... */
529633
const settingsOps = [{
530634
label: "Multi-line input",
531635
boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti,
@@ -540,56 +644,28 @@
540644
Chat.settings.set('monospace-messages',
541645
document.body.classList.contains('monospace-messages'));
542646
}
543647
},{
544648
label: "Chat-only mode",
545
- boolValue: ()=>!!document.body.classList.contains('chat-only-mode'),
546
- callback: function f(){
547
- if(undefined === f.isHidden){
548
- f.isHidden = false;
549
- f.elemsToToggle = [];
550
- document.body.childNodes.forEach(function(e){
551
- if(!e.classList) return/*TEXT nodes and such*/;
552
- else if(!e.classList.contains('content')
553
- && !e.classList.contains('fossil-PopupWidget')
554
- /*kludge^^^ for settingsPopup click handling!*/){
555
- f.elemsToToggle.push(e);
556
- }
557
- });
558
- /* In order to make the input area opaque, such that the
559
- message list scrolls under it without being visible, we
560
- have to ensure that the input area has a non-inherited
561
- background color. Ideally we'd select the color of
562
- div.content, but that is not necessarily set, so we fall
563
- back to using the body's background color. If we rely on
564
- the input area having its own color specified in CSS then
565
- all skins would have to define an appropriate color.
566
- Thus our selection of the body color, while slightly unfortunate,
567
- is in the interest of keeping skins from being forced to
568
- define an opaque bg color.
569
- */
570
- f.initialBg = Chat.e.messagesWrapper.style.backgroundColor;
571
- const cs = window.getComputedStyle(document.body);
572
- f.inheritedBg = cs.backgroundColor;
573
- }
574
- const iws = Chat.e.inputWrapper.style;
575
- if((f.isHidden = !f.isHidden)){
576
- D.addClass(f.elemsToToggle, 'hidden');
577
- D.addClass(document.body, 'chat-only-mode');
578
- iws.backgroundColor = f.inheritedBg;
579
- }else{
580
- D.removeClass(f.elemsToToggle, 'hidden');
581
- D.removeClass(document.body, 'chat-only-mode');
582
- iws.backgroundColor = f.initialBg;
583
- }
649
+ boolValue: ()=>Chat.isChatOnlyMode(),
650
+ callback: function(){
651
+ Chat.toggleChatOnlyMode();
584652
}
585653
},{
586654
label: "Left-align my posts",
587655
boolValue: ()=>!document.body.classList.contains('my-messages-right'),
588656
callback: function f(){
589657
document.body.classList.toggle('my-messages-right');
590658
}
659
+ },{
660
+ label: "Bottom-up chat",
661
+ boolValue: ()=>document.body.classList.contains('chat-bottom-up'),
662
+ callback: function(){
663
+ document.body.classList.toggle('chat-bottom-up');
664
+ Chat.settings.set('bottom-up',
665
+ document.body.classList.contains('chat-bottom-up'));
666
+ }
591667
},{
592668
label: "Images inline",
593669
boolValue: ()=>Chat.settings.getBool('images-inline'),
594670
callback: function(){
595671
const v = Chat.settings.getBool('images-inline',true);
@@ -620,33 +696,43 @@
620696
}
621697
D.append(settingsPopup.e, line);
622698
btn.addEventListener('click', callback);
623699
});
624700
};
625
- /**
626
- Reminder:
627
- settingsPopup.installClickToHide();
628
- Don't do this for this popup! It interferes with the embedded
629
- "?" buttons in the popup, which are also PopupWidget users.
630
- */
701
+ settingsPopup.installHideHandlers(false, true, true)
702
+ /** Reminder: click-to-hide interferes with "?" embedded within
703
+ the popup, so cannot be used together with those. Enabling
704
+ this means, however, that tapping the menu button to toggle
705
+ the menu cannot work because tapping the menu button while the
706
+ menu is opened will, because of the click-to-hide handler,
707
+ hide the menu before the button gets an event saying to toggle
708
+ it.*/;
631709
D.attr(settingsButton, 'role', 'button');
632710
settingsButton.addEventListener('click',function(ev){
633711
//ev.preventDefault();
634712
if(settingsPopup.isShown()) settingsPopup.hide();
635713
else settingsPopup.show(settingsButton);
636714
/* Reminder: we cannot toggle the visibility from her
637715
*/
638716
}, false);
639717
640
- /* Find an ideal X position for the popup, directly under the settings
718
+ /* Find an ideal X/Y position for the popup, directly above the settings
641719
button, based on the size of the popup... */
642720
settingsPopup.show(document.body);
643721
popupSize = settingsPopup.e.getBoundingClientRect();
644722
settingsPopup.hide();
645723
settingsPopup.options.adjustX = function(x){
646724
const rect = settingsButton.getBoundingClientRect();
647725
return rect.right - popupSize.width;
726
+ };
727
+ settingsPopup.options.adjustY = function(y){
728
+ const rect = settingsButton.getBoundingClientRect();
729
+ if(Chat.isUiFlipped()){
730
+ return rect.top - popupSize.height -2;
731
+ }else{
732
+ return rect.bottom + 2;
733
+ }
648734
};
649735
})()/*#chat-settings-button setup*/;
650736
651737
652738
/** Callback for poll() to inject new content into the page. jx ==
@@ -756,14 +842,17 @@
756842
.catch(e=>console.error(e))
757843
/* ^^^ we don't use Chat.reportError(e) here b/c the polling
758844
fails exepectedly when it times out, but is then immediately
759845
resumed, and reportError() produces a loud error message. */
760846
.finally(function(x){
761
- if(isFirstCall) Chat.ajaxEnd();
847
+ if(isFirstCall){
848
+ Chat.ajaxEnd();
849
+ Chat.e.inputWrapper.scrollIntoView();
850
+ }
762851
poll.running=false;
763852
});
764853
}
765854
poll.running = false;
766855
poll(true);
767856
setInterval(poll, 1000);
768857
F.page.chat = Chat/* enables testing the APIs via the dev tools */;
769858
})();
770859
--- src/chat.js
+++ src/chat.js
@@ -7,24 +7,27 @@
7 const E1 = function(selector){
8 const e = document.querySelector(selector);
9 if(!e) throw new Error("missing required DOM element: "+selector);
10 return e;
11 };
 
12 const Chat = (function(){
13 const cs = {
14 e:{/*map of certain DOM elements.*/
15 messageInjectPoint: E1('#message-inject-point'),
16 pageTitle: E1('head title'),
17 loadToolbar: undefined /* the load-posts toolbar (dynamically created) */,
18 inputWrapper: E1("#chat-input-area"),
 
19 messagesWrapper: E1('#chat-messages-wrapper'),
20 inputForm: E1('#chat-form'),
21 btnSubmit: E1('#chat-message-submit'),
22 inputSingle: E1('#chat-input-single'),
23 inputMulti: E1('#chat-input-multi'),
24 inputCurrent: undefined/*one of inputSingle or inputMulti*/,
25 inputFile: E1('#chat-input-file')
 
26 },
27 me: F.user.name,
28 mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
29 mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
30 pageIsActive: 'visible'===document.visibilityState,
@@ -63,10 +66,11 @@
63 this.e.inputCurrent = this.e.inputSingle;
64 }
65 D.addClass(old, 'hidden');
66 D.removeClass(this.e.inputCurrent, 'hidden');
67 this.e.inputCurrent.value = old.value;
 
68 return this;
69 },
70 /** Enables (if yes is truthy) or disables all elements in
71 * this.disableDuringAjax. */
72 enableAjaxComponents: function(yes){
@@ -112,43 +116,141 @@
112 injectMessageElem: function f(e, atEnd){
113 const mip = atEnd ? this.e.loadToolbar : this.e.messageInjectPoint;
114 if(atEnd){
115 mip.parentNode.insertBefore(e, mip);
116 }else{
117 if(mip.nextSibling){
118 mip.parentNode.insertBefore(e, mip.nextSibling);
119 }else{
120 mip.parentNode.appendChild(e);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121 }
122 }
123 },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124 settings:{
125 get: (k,dflt)=>F.storage.get(k,dflt),
126 getBool: (k,dflt)=>F.storage.getBool(k,dflt),
127 set: (k,v)=>F.storage.set(k,v),
128 defaults:{
129 "images-inline": !!F.config.chat.imagesInline,
130 "monospace-messages": false
 
131 }
132 }
133 };
134 Object.keys(cs.settings.defaults).forEach(function f(k){
135 const v = cs.settings.get(k,f);
136 if(f===v) cs.settings.set(k,cs.settings.defaults[k]);
 
137 });
138 if(window.innerWidth<window.innerHeight){
139 /* Alignment of 'my' messages: right alignment is conventional
140 for mobile chat apps but can be difficult to read in wide
141 windows (desktop/tablet landscape mode). Can be toggled via
142 settings popup. */
 
143 document.body.classList.add('my-messages-right');
144 }
 
 
 
145 if(cs.settings.getBool('monospace-messages',false)){
146 document.body.classList.add('monospace-messages');
147 }
148 cs.e.inputCurrent = cs.e.inputSingle;
149 cs.pageTitleOrig = cs.e.pageTitle.innerText;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150 const qs = (e)=>document.querySelector(e);
151 const argsToArray = function(args){
152 return Array.prototype.slice.call(args,0);
153 };
154 cs.reportError = function(/*msg args*/){
@@ -265,12 +367,15 @@
265 this.e.content.style.backgroundColor = m.uclr;
266 this.e.tab.style.backgroundColor = m.uclr;
267
268 const d = new Date(m.mtime);
269 D.append(
270 D.clearElement(this.e.tab), D.text(
271 m.xfrom+' @ '+d.getHours()+":"+(d.getMinutes()+100).toString().slice(1,3))
 
 
 
272 );
273 var contentTarget = this.e.content;
274 if( m.fsize>0 ){
275 if( m.fmime
276 && m.fmime.startsWith("image/")
@@ -285,14 +390,17 @@
285 "(" + m.fname + " " + m.fsize + " bytes)"
286 )
287 D.attr(a,'target','_blank');
288 contentTarget.appendChild(a);
289 }
290 contentTarget = D.div();
291 }
292 if(m.xmsg){
293 if(contentTarget !== this.e.content){
 
 
 
294 D.append(this.e.content, contentTarget);
295 }
296 // The m.xmsg text comes from the same server as this script and
297 // is guaranteed by that server to be "safe" HTML - safe in the
298 // sense that it is not possible for a malefactor to inject HTML
@@ -361,11 +469,11 @@
361 }
362 }, false);
363 /* Add help button for drag/drop/paste zone */
364 Chat.e.inputFile.parentNode.insertBefore(
365 F.helpButtonlets.create(
366 document.querySelector('#chat-input-file-area .help-buttonlet')
367 ), Chat.e.inputFile
368 );
369 ////////////////////////////////////////////////////////////
370 // File drag/drop visual notification.
371 const dropHighlight = Chat.e.inputFile /* target zone */;
@@ -490,11 +598,11 @@
490 Chat.deleteMessage(eMsg);
491 });
492 }
493 }/*refresh()*/
494 });
495 f.popup.installClickToHide();
496 f.popup.hide = function(){
497 delete this._eMsg;
498 D.clearElement(this.e);
499 return this.show(false);
500 };
@@ -517,15 +625,11 @@
517
518 (function(){/*Set up #chat-settings-button */
519 const settingsButton = document.querySelector('#chat-settings-button');
520 var popupSize = undefined/*placement workaround*/;
521 const settingsPopup = new F.PopupWidget({
522 cssClass: ['fossil-tooltip', 'chat-settings-popup'],
523 adjustY: function(y){
524 const rect = settingsButton.getBoundingClientRect();
525 return rect.top + rect.height + 2;
526 }
527 });
528 /* Settings menu entries... */
529 const settingsOps = [{
530 label: "Multi-line input",
531 boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti,
@@ -540,56 +644,28 @@
540 Chat.settings.set('monospace-messages',
541 document.body.classList.contains('monospace-messages'));
542 }
543 },{
544 label: "Chat-only mode",
545 boolValue: ()=>!!document.body.classList.contains('chat-only-mode'),
546 callback: function f(){
547 if(undefined === f.isHidden){
548 f.isHidden = false;
549 f.elemsToToggle = [];
550 document.body.childNodes.forEach(function(e){
551 if(!e.classList) return/*TEXT nodes and such*/;
552 else if(!e.classList.contains('content')
553 && !e.classList.contains('fossil-PopupWidget')
554 /*kludge^^^ for settingsPopup click handling!*/){
555 f.elemsToToggle.push(e);
556 }
557 });
558 /* In order to make the input area opaque, such that the
559 message list scrolls under it without being visible, we
560 have to ensure that the input area has a non-inherited
561 background color. Ideally we'd select the color of
562 div.content, but that is not necessarily set, so we fall
563 back to using the body's background color. If we rely on
564 the input area having its own color specified in CSS then
565 all skins would have to define an appropriate color.
566 Thus our selection of the body color, while slightly unfortunate,
567 is in the interest of keeping skins from being forced to
568 define an opaque bg color.
569 */
570 f.initialBg = Chat.e.messagesWrapper.style.backgroundColor;
571 const cs = window.getComputedStyle(document.body);
572 f.inheritedBg = cs.backgroundColor;
573 }
574 const iws = Chat.e.inputWrapper.style;
575 if((f.isHidden = !f.isHidden)){
576 D.addClass(f.elemsToToggle, 'hidden');
577 D.addClass(document.body, 'chat-only-mode');
578 iws.backgroundColor = f.inheritedBg;
579 }else{
580 D.removeClass(f.elemsToToggle, 'hidden');
581 D.removeClass(document.body, 'chat-only-mode');
582 iws.backgroundColor = f.initialBg;
583 }
584 }
585 },{
586 label: "Left-align my posts",
587 boolValue: ()=>!document.body.classList.contains('my-messages-right'),
588 callback: function f(){
589 document.body.classList.toggle('my-messages-right');
590 }
 
 
 
 
 
 
 
 
591 },{
592 label: "Images inline",
593 boolValue: ()=>Chat.settings.getBool('images-inline'),
594 callback: function(){
595 const v = Chat.settings.getBool('images-inline',true);
@@ -620,33 +696,43 @@
620 }
621 D.append(settingsPopup.e, line);
622 btn.addEventListener('click', callback);
623 });
624 };
625 /**
626 Reminder:
627 settingsPopup.installClickToHide();
628 Don't do this for this popup! It interferes with the embedded
629 "?" buttons in the popup, which are also PopupWidget users.
630 */
 
 
631 D.attr(settingsButton, 'role', 'button');
632 settingsButton.addEventListener('click',function(ev){
633 //ev.preventDefault();
634 if(settingsPopup.isShown()) settingsPopup.hide();
635 else settingsPopup.show(settingsButton);
636 /* Reminder: we cannot toggle the visibility from her
637 */
638 }, false);
639
640 /* Find an ideal X position for the popup, directly under the settings
641 button, based on the size of the popup... */
642 settingsPopup.show(document.body);
643 popupSize = settingsPopup.e.getBoundingClientRect();
644 settingsPopup.hide();
645 settingsPopup.options.adjustX = function(x){
646 const rect = settingsButton.getBoundingClientRect();
647 return rect.right - popupSize.width;
 
 
 
 
 
 
 
 
648 };
649 })()/*#chat-settings-button setup*/;
650
651
652 /** Callback for poll() to inject new content into the page. jx ==
@@ -756,14 +842,17 @@
756 .catch(e=>console.error(e))
757 /* ^^^ we don't use Chat.reportError(e) here b/c the polling
758 fails exepectedly when it times out, but is then immediately
759 resumed, and reportError() produces a loud error message. */
760 .finally(function(x){
761 if(isFirstCall) Chat.ajaxEnd();
 
 
 
762 poll.running=false;
763 });
764 }
765 poll.running = false;
766 poll(true);
767 setInterval(poll, 1000);
768 F.page.chat = Chat/* enables testing the APIs via the dev tools */;
769 })();
770
--- src/chat.js
+++ src/chat.js
@@ -7,24 +7,27 @@
7 const E1 = function(selector){
8 const e = document.querySelector(selector);
9 if(!e) throw new Error("missing required DOM element: "+selector);
10 return e;
11 };
12 //document.body.classList.add('chat-only-mode');
13 const Chat = (function(){
14 const cs = {
15 e:{/*map of certain DOM elements.*/
16 messageInjectPoint: E1('#message-inject-point'),
17 pageTitle: E1('head title'),
18 loadToolbar: undefined /* the load-posts toolbar (dynamically created) */,
19 inputWrapper: E1("#chat-input-area"),
20 fileSelectWrapper: E1('#chat-input-file-area'),
21 messagesWrapper: E1('#chat-messages-wrapper'),
22 inputForm: E1('#chat-form'),
23 btnSubmit: E1('#chat-message-submit'),
24 inputSingle: E1('#chat-input-single'),
25 inputMulti: E1('#chat-input-multi'),
26 inputCurrent: undefined/*one of inputSingle or inputMulti*/,
27 inputFile: E1('#chat-input-file'),
28 contentDiv: E1('div.content')
29 },
30 me: F.user.name,
31 mxMsg: F.config.chat.initSize ? -F.config.chat.initSize : -50,
32 mnMsg: undefined/*lowest message ID we've seen so far (for history loading)*/,
33 pageIsActive: 'visible'===document.visibilityState,
@@ -63,10 +66,11 @@
66 this.e.inputCurrent = this.e.inputSingle;
67 }
68 D.addClass(old, 'hidden');
69 D.removeClass(this.e.inputCurrent, 'hidden');
70 this.e.inputCurrent.value = old.value;
71 old.value = '';
72 return this;
73 },
74 /** Enables (if yes is truthy) or disables all elements in
75 * this.disableDuringAjax. */
76 enableAjaxComponents: function(yes){
@@ -112,43 +116,141 @@
116 injectMessageElem: function f(e, atEnd){
117 const mip = atEnd ? this.e.loadToolbar : this.e.messageInjectPoint;
118 if(atEnd){
119 mip.parentNode.insertBefore(e, mip);
120 }else{
121 const self = this;
122 if(false && 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
130 The remaining problem here is messages with IMG tags.
131 At this point in the process their IMG.src has not yet
132 been loaded - that's async. We scroll the message into
133 view, but then the downstream loading of IMG.src pushes
134 the message content back down, sliding the message
135 behind the input field. This can be verified by delaying the
136 message scroll by a second or so to give the image time
137 to load (from a local server instance).
138 */
139 D.addClass(self.e.inputWrapper,'unsticky');
140 }
141 if(mip.nextSibling) mip.parentNode.insertBefore(e, mip.nextSibling);
142 else mip.parentNode.appendChild(e);
143 if(false && this.isUiFlipped()){
144 //e.scrollIntoView();
145 setTimeout(function(){
146 //self.e.inputWrapper.scrollIntoView();
147 //self.e.fileSelectWrapper.scrollIntoView();
148 //e.scrollIntoView();
149 //D.removeClass(self.e.inputWrapper,'unsticky');
150 self.e.inputWrapper.scrollIntoView();
151 },0);
152 }
153 }
154 },
155 /** Returns true if chat-only mode is enabled. */
156 isChatOnlyMode: ()=>document.body.classList.contains('chat-only-mode'),
157 /** Returns true if the UI seems to be in "bottom-up" mode. */
158 isUiFlipped: function(){
159 const style = window.getComputedStyle(this.e.contentDiv);
160 return style.flexDirection.indexOf("-reverse")>0;
161 },
162 /**
163 Enters (if passed a truthy value or no arguments) or leaves
164 "chat-only" mode. That mode hides the page's header and
165 footer, leaving only the chat application visible to the
166 user.
167 */
168 chatOnlyMode: function f(yes){
169 if(undefined === f.elemsToToggle){
170 f.elemsToToggle = [];
171 document.body.childNodes.forEach(function(e){
172 if(!e.classList) return/*TEXT nodes and such*/;
173 else if(!e.classList.contains('content')
174 && !e.classList.contains('fossil-PopupWidget')
175 /*kludge^^^ for settingsPopup click handling!*/){
176 f.elemsToToggle.push(e);
177 }
178 });
179 }
180 if(!arguments.length) yes = true;
181 if(yes === this.isChatOnlyMode()) return this;
182 if(yes){
183 D.addClass(f.elemsToToggle, 'hidden');
184 D.addClass(document.body, 'chat-only-mode');
185 document.body.scroll(0,document.body.height);
186 }else{
187 D.removeClass(f.elemsToToggle, 'hidden');
188 D.removeClass(document.body, 'chat-only-mode');
189 setTimeout(()=>document.body.scrollIntoView(
190 /*moves to (0,0), whereas scrollTo(0,0) does not!
191 setTimeout() is unfortunately necessary to get the scroll
192 placement correct.*/
193 ), 0);
194 }
195 const msg = document.querySelector('.message-widget');
196 if(msg) setTimeout(()=>msg.scrollIntoView(),0);
197 return this;
198 },
199 toggleChatOnlyMode: function(){
200 return this.chatOnlyMode(!this.isChatOnlyMode());
201 },
202 settings:{
203 get: (k,dflt)=>F.storage.get(k,dflt),
204 getBool: (k,dflt)=>F.storage.getBool(k,dflt),
205 set: (k,v)=>F.storage.set(k,v),
206 defaults:{
207 "images-inline": !!F.config.chat.imagesInline,
208 "monospace-messages": false,
209 "bottom-up": true
210 }
211 }
212 };
213 /* Install default settings... */
214 Object.keys(cs.settings.defaults).forEach(function(k){
215 const v = cs.settings.get(k,cs);
216 if(cs===v) cs.settings.set(k,cs.settings.defaults[k]);
217 });
218 if(window.innerWidth<window.innerHeight){
219 /* Alignment of 'my' messages: right alignment is conventional
220 for mobile chat apps but can be difficult to read in wide
221 windows (desktop/tablet landscape mode), so we default to a
222 layout based on the apparently "orientation" of the window:
223 tall vs wide. Can be toggled via settings popup. */
224 document.body.classList.add('my-messages-right');
225 }
226 if(cs.settings.getBool("bottom-up")){
227 document.body.classList.add('chat-bottom-up');
228 }
229 if(cs.settings.getBool('monospace-messages',false)){
230 document.body.classList.add('monospace-messages');
231 }
232 cs.e.inputCurrent = cs.e.inputSingle;
233 cs.pageTitleOrig = cs.e.pageTitle.innerText;
234
235 if(true){
236 /* In order to make the input area opaque, such that the message
237 list scrolls under it without being visible, we have to
238 ensure that the input area has a non-transparent background
239 color. Ideally we'd select the color of div.content, but that
240 is not necessarily set, so we fall back to using the body's
241 background color and hope it's been explicitly set
242 somewhere. If we rely on the input area having its own color
243 specified in CSS then all skins would have to define an
244 appropriate color. Thus our selection of the body color,
245 while slightly unfortunate, is in the interest of keeping
246 skins from being forced to define an opaque bg color.
247 */
248 const bodyStyle = window.getComputedStyle(document.body);
249 cs.e.inputWrapper.style.backgroundColor = bodyStyle.backgroundColor;
250 }
251
252 const qs = (e)=>document.querySelector(e);
253 const argsToArray = function(args){
254 return Array.prototype.slice.call(args,0);
255 };
256 cs.reportError = function(/*msg args*/){
@@ -265,12 +367,15 @@
367 this.e.content.style.backgroundColor = m.uclr;
368 this.e.tab.style.backgroundColor = m.uclr;
369
370 const d = new Date(m.mtime);
371 D.append(
372 D.clearElement(this.e.tab),
373 D.text(
374 m.xfrom," #",m.msgid,' @ ',d.getHours(),":",
375 (d.getMinutes()+100).toString().slice(1,3)
376 )
377 );
378 var contentTarget = this.e.content;
379 if( m.fsize>0 ){
380 if( m.fmime
381 && m.fmime.startsWith("image/")
@@ -285,14 +390,17 @@
390 "(" + m.fname + " " + m.fsize + " bytes)"
391 )
392 D.attr(a,'target','_blank');
393 contentTarget.appendChild(a);
394 }
395 ;
396 }
397 if(m.xmsg){
398 if(m.fsize>0){
399 /* We have file/image content, so need another element for
400 the message text. */
401 contentTarget = D.div();
402 D.append(this.e.content, contentTarget);
403 }
404 // The m.xmsg text comes from the same server as this script and
405 // is guaranteed by that server to be "safe" HTML - safe in the
406 // sense that it is not possible for a malefactor to inject HTML
@@ -361,11 +469,11 @@
469 }
470 }, false);
471 /* Add help button for drag/drop/paste zone */
472 Chat.e.inputFile.parentNode.insertBefore(
473 F.helpButtonlets.create(
474 Chat.e.fileSelectWrapper.querySelector('.help-buttonlet')
475 ), Chat.e.inputFile
476 );
477 ////////////////////////////////////////////////////////////
478 // File drag/drop visual notification.
479 const dropHighlight = Chat.e.inputFile /* target zone */;
@@ -490,11 +598,11 @@
598 Chat.deleteMessage(eMsg);
599 });
600 }
601 }/*refresh()*/
602 });
603 f.popup.installHideHandlers();
604 f.popup.hide = function(){
605 delete this._eMsg;
606 D.clearElement(this.e);
607 return this.show(false);
608 };
@@ -517,15 +625,11 @@
625
626 (function(){/*Set up #chat-settings-button */
627 const settingsButton = document.querySelector('#chat-settings-button');
628 var popupSize = undefined/*placement workaround*/;
629 const settingsPopup = new F.PopupWidget({
630 cssClass: ['fossil-tooltip', 'chat-settings-popup']
 
 
 
 
631 });
632 /* Settings menu entries... */
633 const settingsOps = [{
634 label: "Multi-line input",
635 boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti,
@@ -540,56 +644,28 @@
644 Chat.settings.set('monospace-messages',
645 document.body.classList.contains('monospace-messages'));
646 }
647 },{
648 label: "Chat-only mode",
649 boolValue: ()=>Chat.isChatOnlyMode(),
650 callback: function(){
651 Chat.toggleChatOnlyMode();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
652 }
653 },{
654 label: "Left-align my posts",
655 boolValue: ()=>!document.body.classList.contains('my-messages-right'),
656 callback: function f(){
657 document.body.classList.toggle('my-messages-right');
658 }
659 },{
660 label: "Bottom-up chat",
661 boolValue: ()=>document.body.classList.contains('chat-bottom-up'),
662 callback: function(){
663 document.body.classList.toggle('chat-bottom-up');
664 Chat.settings.set('bottom-up',
665 document.body.classList.contains('chat-bottom-up'));
666 }
667 },{
668 label: "Images inline",
669 boolValue: ()=>Chat.settings.getBool('images-inline'),
670 callback: function(){
671 const v = Chat.settings.getBool('images-inline',true);
@@ -620,33 +696,43 @@
696 }
697 D.append(settingsPopup.e, line);
698 btn.addEventListener('click', callback);
699 });
700 };
701 settingsPopup.installHideHandlers(false, true, true)
702 /** Reminder: click-to-hide interferes with "?" embedded within
703 the popup, so cannot be used together with those. Enabling
704 this means, however, that tapping the menu button to toggle
705 the menu cannot work because tapping the menu button while the
706 menu is opened will, because of the click-to-hide handler,
707 hide the menu before the button gets an event saying to toggle
708 it.*/;
709 D.attr(settingsButton, 'role', 'button');
710 settingsButton.addEventListener('click',function(ev){
711 //ev.preventDefault();
712 if(settingsPopup.isShown()) settingsPopup.hide();
713 else settingsPopup.show(settingsButton);
714 /* Reminder: we cannot toggle the visibility from her
715 */
716 }, false);
717
718 /* Find an ideal X/Y position for the popup, directly above the settings
719 button, based on the size of the popup... */
720 settingsPopup.show(document.body);
721 popupSize = settingsPopup.e.getBoundingClientRect();
722 settingsPopup.hide();
723 settingsPopup.options.adjustX = function(x){
724 const rect = settingsButton.getBoundingClientRect();
725 return rect.right - popupSize.width;
726 };
727 settingsPopup.options.adjustY = function(y){
728 const rect = settingsButton.getBoundingClientRect();
729 if(Chat.isUiFlipped()){
730 return rect.top - popupSize.height -2;
731 }else{
732 return rect.bottom + 2;
733 }
734 };
735 })()/*#chat-settings-button setup*/;
736
737
738 /** Callback for poll() to inject new content into the page. jx ==
@@ -756,14 +842,17 @@
842 .catch(e=>console.error(e))
843 /* ^^^ we don't use Chat.reportError(e) here b/c the polling
844 fails exepectedly when it times out, but is then immediately
845 resumed, and reportError() produces a loud error message. */
846 .finally(function(x){
847 if(isFirstCall){
848 Chat.ajaxEnd();
849 Chat.e.inputWrapper.scrollIntoView();
850 }
851 poll.running=false;
852 });
853 }
854 poll.running = false;
855 poll(true);
856 setInterval(poll, 1000);
857 F.page.chat = Chat/* enables testing the APIs via the dev tools */;
858 })();
859
+39 -34
--- 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;
@@ -1598,12 +1595,14 @@
15981595
border: 1px outset rgba(127,127,127,1);
15991596
}
16001597
body.fossil-dark-style .settings-icon {
16011598
filter: invert(100%);
16021599
}
1603
-body.chat #chat-settings-button {
1604
-}
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 {}
16051604
/** Popup widget for the /chat settings. */
16061605
body.chat .chat-settings-popup {
16071606
font-size: 0.8em;
16081607
text-align: left;
16091608
display: flex;
@@ -1640,49 +1639,53 @@
16401639
/** Container for the list of /chat messages. */
16411640
body.chat #chat-messages-wrapper {
16421641
display: flex;
16431642
flex-direction: column;
16441643
}
1645
-/* "Chat-only mode" hides the site header/footer, showing only
1646
- the chat app. */
1647
-body.chat.chat-only-mode{
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!*/;
16481649
}
1649
-body.chat.chat-only-mode > div.content {
1650
+body.chat > div.content {
16501651
margin: 0;
16511652
padding: 0;
16521653
display: flex;
16531654
flex-direction: column;
16541655
align-items: stretch;
16551656
}
1656
-body.chat.chat-only-mode #chat-input-area {
1657
- /* would like to pin this to the top so that it stays in place when
1658
- scrolling, but doing so causes #chat-messages-wrapper to scroll
1659
- behind it visibly, which is really ugly. Only current workaround is
1660
- to force an opaque background color on this element, but that's not
1661
- skin-friendly. */
1662
- position: sticky;
1663
- position: -webkit-sticky /* supposedly some versions of Safari */;
1664
- top: 0;
1665
- padding: 0.5em 1em;
1666
- z-index: 100
1667
- /* see notes in #chat-messages-wrapper. The various popups require a
1668
- z-index higher than this one. */
1669
-}
1670
-body.chat.chat-only-mode #chat-messages-wrapper {
1671
- position: relative;
1672
- top: 0;
1673
- z-index: 99 /* so that it scrolls under input area. If it's
1674
- lower than div.content then mouse events to it
1675
- are blocked!*/;
1657
+body.chat.chat-bottom-up > div.content {
1658
+ flex-direction: column-reverse;
16761659
}
16771660
/* Wrapper for /chat user input controls */
16781661
body.chat #chat-input-area {
16791662
display: flex;
16801663
flex-direction: column;
16811664
border-bottom: 1px solid black;
1665
+ padding: 0.5em 1em;
16821666
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: initial /*sticky currently disabled due to scrolling-related issues*/;
1678
+ bottom: 0;
1679
+}
1680
+/* An internal hack to try to help resolve a message-scrolling quirk
1681
+ when #chat-input-area is sticky on the bottom of the screen. */
1682
+body.chat.chat-bottom-up #chat-input-area.unsticky {
1683
+ position: initial;
16831684
}
1685
+/* Widget holding the chat message input field, send button, and
1686
+ settings button. */
16841687
body.chat #chat-input-line {
16851688
display: flex;
16861689
flex-direction: row;
16871690
margin-bottom: 0.25em;
16881691
align-items: flex-start;
@@ -1694,10 +1697,11 @@
16941697
}
16951698
body.chat #chat-input-line > input[type=text],
16961699
body.chat #chat-input-line > textarea {
16971700
flex: 5 1 auto;
16981701
}
1702
+/* Widget holding the file selection control and preview */
16991703
body.chat #chat-input-file-area {
17001704
display: flex;
17011705
flex-direction: row;
17021706
align-items: center;
17031707
flex-wrap: wrap;
@@ -1718,16 +1722,17 @@
17181722
padding: 0.25em;
17191723
}
17201724
body.chat #chat-input-file > input {
17211725
flex: 1 0 auto;
17221726
}
1727
+/* Indicator when a drag/drop is in progress */
17231728
body.chat #chat-input-file.dragover {
17241729
border: 1px dashed green;
17251730
}
1731
+/* Widget holding the details of a selected/dropped file/image. */
17261732
body.chat #chat-drop-details {
17271733
flex: 0 1 auto;
17281734
padding: 0.5em 1em;
17291735
margin-left: 0.5em;
17301736
white-space: pre;
17311737
font-family: monospace;
1732
- max-width: 50%;
17331738
}
17341739
--- 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;
@@ -1598,12 +1595,14 @@
1598 border: 1px outset rgba(127,127,127,1);
1599 }
1600 body.fossil-dark-style .settings-icon {
1601 filter: invert(100%);
1602 }
1603 body.chat #chat-settings-button {
1604 }
 
 
1605 /** Popup widget for the /chat settings. */
1606 body.chat .chat-settings-popup {
1607 font-size: 0.8em;
1608 text-align: left;
1609 display: flex;
@@ -1640,49 +1639,53 @@
1640 /** Container for the list of /chat messages. */
1641 body.chat #chat-messages-wrapper {
1642 display: flex;
1643 flex-direction: column;
1644 }
1645 /* "Chat-only mode" hides the site header/footer, showing only
1646 the chat app. */
1647 body.chat.chat-only-mode{
 
 
1648 }
1649 body.chat.chat-only-mode > div.content {
1650 margin: 0;
1651 padding: 0;
1652 display: flex;
1653 flex-direction: column;
1654 align-items: stretch;
1655 }
1656 body.chat.chat-only-mode #chat-input-area {
1657 /* would like to pin this to the top so that it stays in place when
1658 scrolling, but doing so causes #chat-messages-wrapper to scroll
1659 behind it visibly, which is really ugly. Only current workaround is
1660 to force an opaque background color on this element, but that's not
1661 skin-friendly. */
1662 position: sticky;
1663 position: -webkit-sticky /* supposedly some versions of Safari */;
1664 top: 0;
1665 padding: 0.5em 1em;
1666 z-index: 100
1667 /* see notes in #chat-messages-wrapper. The various popups require a
1668 z-index higher than this one. */
1669 }
1670 body.chat.chat-only-mode #chat-messages-wrapper {
1671 position: relative;
1672 top: 0;
1673 z-index: 99 /* so that it scrolls under input area. If it's
1674 lower than div.content then mouse events to it
1675 are blocked!*/;
1676 }
1677 /* Wrapper for /chat user input controls */
1678 body.chat #chat-input-area {
1679 display: flex;
1680 flex-direction: column;
1681 border-bottom: 1px solid black;
 
1682 margin-bottom: 0.5em;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1683 }
 
 
1684 body.chat #chat-input-line {
1685 display: flex;
1686 flex-direction: row;
1687 margin-bottom: 0.25em;
1688 align-items: flex-start;
@@ -1694,10 +1697,11 @@
1694 }
1695 body.chat #chat-input-line > input[type=text],
1696 body.chat #chat-input-line > textarea {
1697 flex: 5 1 auto;
1698 }
 
1699 body.chat #chat-input-file-area {
1700 display: flex;
1701 flex-direction: row;
1702 align-items: center;
1703 flex-wrap: wrap;
@@ -1718,16 +1722,17 @@
1718 padding: 0.25em;
1719 }
1720 body.chat #chat-input-file > input {
1721 flex: 1 0 auto;
1722 }
 
1723 body.chat #chat-input-file.dragover {
1724 border: 1px dashed green;
1725 }
 
1726 body.chat #chat-drop-details {
1727 flex: 0 1 auto;
1728 padding: 0.5em 1em;
1729 margin-left: 0.5em;
1730 white-space: pre;
1731 font-family: monospace;
1732 max-width: 50%;
1733 }
1734
--- 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;
@@ -1598,12 +1595,14 @@
1595 border: 1px outset rgba(127,127,127,1);
1596 }
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;
@@ -1640,49 +1639,53 @@
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: initial /*sticky currently disabled due to scrolling-related issues*/;
1678 bottom: 0;
1679 }
1680 /* An internal hack to try to help resolve a message-scrolling quirk
1681 when #chat-input-area is sticky on the bottom of the screen. */
1682 body.chat.chat-bottom-up #chat-input-area.unsticky {
1683 position: initial;
1684 }
1685 /* Widget holding the chat message input field, send button, and
1686 settings button. */
1687 body.chat #chat-input-line {
1688 display: flex;
1689 flex-direction: row;
1690 margin-bottom: 0.25em;
1691 align-items: flex-start;
@@ -1694,10 +1697,11 @@
1697 }
1698 body.chat #chat-input-line > input[type=text],
1699 body.chat #chat-input-line > textarea {
1700 flex: 5 1 auto;
1701 }
1702 /* Widget holding the file selection control and preview */
1703 body.chat #chat-input-file-area {
1704 display: flex;
1705 flex-direction: row;
1706 align-items: center;
1707 flex-wrap: wrap;
@@ -1718,16 +1722,17 @@
1722 padding: 0.25em;
1723 }
1724 body.chat #chat-input-file > input {
1725 flex: 1 0 auto;
1726 }
1727 /* Indicator when a drag/drop is in progress */
1728 body.chat #chat-input-file.dragover {
1729 border: 1px dashed green;
1730 }
1731 /* Widget holding the details of a selected/dropped file/image. */
1732 body.chat #chat-drop-details {
1733 flex: 0 1 auto;
1734 padding: 0.5em 1em;
1735 margin-left: 0.5em;
1736 white-space: pre;
1737 font-family: monospace;
 
1738 }
1739
--- 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