Fossil SCM

Significant reworking of chat input mode to use a single contenteditable element instead of two text input elements. This required considerable collateral cleanup in how the various settings are handled and communicated within the app.

stephan 2021-09-29 22:08 trunk
Commit b9c5792e34bc133d0ec383ef30a38510d73051b43d2f645d620a75ce736c26ee
+2 -7
--- src/chat.c
+++ src/chat.c
@@ -142,12 +142,10 @@
142142
** send new chat message, delete older messages, or poll for changes.
143143
*/
144144
void chat_webpage(void){
145145
char *zAlert;
146146
char *zProjectName;
147
- const char * zInputPlaceholder1 = /* Placeholder for 1-line input */
148
- "Enter sends and Shift-Enter previews.";
149147
const char * zInputPlaceholder2 = /* Placeholder for textarea input*/
150148
"Ctrl-Enter sends and Shift-Enter previews.";
151149
char * zInputPlaceholder0; /* Common text input placeholder value */
152150
login_check_credentials();
153151
if( !g.perm.Chat ){
@@ -161,16 +159,13 @@
161159
mprintf("Type markdown-formatted message for %h.", zProjectName);
162160
style_set_current_feature("chat");
163161
style_header("Chat");
164162
@ <div id='chat-input-area'>
165163
@ <div id='chat-input-line' class='single-line'>
166
- @ <input type="text" name="msg" id="chat-input-single" \
167
- @ placeholder="%h(zInputPlaceholder0) %h(zInputPlaceholder1)" \
168
- @ autocomplete="off">
169
- @ <textarea rows="8" id="chat-input-multi" \
164
+ @ <div contenteditable id="chat-input-field" \
170165
@ placeholder="%h(zInputPlaceholder0) %h(zInputPlaceholder2)" \
171
- @ class="hidden"></textarea>
166
+ @ class></div>
172167
@ <div id='chat-edit-buttons'>
173168
@ <button id="chat-preview-button" \
174169
@ title="Preview message (Shift-Enter)">&#128065;</button>
175170
@ <button id="chat-settings-button" \
176171
@ title="Configure chat">&#9881;</button>
177172
--- src/chat.c
+++ src/chat.c
@@ -142,12 +142,10 @@
142 ** send new chat message, delete older messages, or poll for changes.
143 */
144 void chat_webpage(void){
145 char *zAlert;
146 char *zProjectName;
147 const char * zInputPlaceholder1 = /* Placeholder for 1-line input */
148 "Enter sends and Shift-Enter previews.";
149 const char * zInputPlaceholder2 = /* Placeholder for textarea input*/
150 "Ctrl-Enter sends and Shift-Enter previews.";
151 char * zInputPlaceholder0; /* Common text input placeholder value */
152 login_check_credentials();
153 if( !g.perm.Chat ){
@@ -161,16 +159,13 @@
161 mprintf("Type markdown-formatted message for %h.", zProjectName);
162 style_set_current_feature("chat");
163 style_header("Chat");
164 @ <div id='chat-input-area'>
165 @ <div id='chat-input-line' class='single-line'>
166 @ <input type="text" name="msg" id="chat-input-single" \
167 @ placeholder="%h(zInputPlaceholder0) %h(zInputPlaceholder1)" \
168 @ autocomplete="off">
169 @ <textarea rows="8" id="chat-input-multi" \
170 @ placeholder="%h(zInputPlaceholder0) %h(zInputPlaceholder2)" \
171 @ class="hidden"></textarea>
172 @ <div id='chat-edit-buttons'>
173 @ <button id="chat-preview-button" \
174 @ title="Preview message (Shift-Enter)">&#128065;</button>
175 @ <button id="chat-settings-button" \
176 @ title="Configure chat">&#9881;</button>
177
--- src/chat.c
+++ src/chat.c
@@ -142,12 +142,10 @@
142 ** send new chat message, delete older messages, or poll for changes.
143 */
144 void chat_webpage(void){
145 char *zAlert;
146 char *zProjectName;
 
 
147 const char * zInputPlaceholder2 = /* Placeholder for textarea input*/
148 "Ctrl-Enter sends and Shift-Enter previews.";
149 char * zInputPlaceholder0; /* Common text input placeholder value */
150 login_check_credentials();
151 if( !g.perm.Chat ){
@@ -161,16 +159,13 @@
159 mprintf("Type markdown-formatted message for %h.", zProjectName);
160 style_set_current_feature("chat");
161 style_header("Chat");
162 @ <div id='chat-input-area'>
163 @ <div id='chat-input-line' class='single-line'>
164 @ <div contenteditable id="chat-input-field" \
 
 
 
165 @ placeholder="%h(zInputPlaceholder0) %h(zInputPlaceholder2)" \
166 @ class></div>
167 @ <div id='chat-edit-buttons'>
168 @ <button id="chat-preview-button" \
169 @ title="Preview message (Shift-Enter)">&#128065;</button>
170 @ <button id="chat-settings-button" \
171 @ title="Configure chat">&#9881;</button>
172
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -127,13 +127,11 @@
127127
inputWrapper: E1("#chat-input-area"),
128128
inputLine: E1('#chat-input-line'),
129129
fileSelectWrapper: E1('#chat-input-file-area'),
130130
viewMessages: E1('#chat-messages-wrapper'),
131131
btnSubmit: E1('#chat-message-submit'),
132
- inputSingle: E1('#chat-input-single'),
133
- inputMulti: E1('#chat-input-multi'),
134
- inputCurrent: undefined/*one of inputSingle or inputMulti*/,
132
+ inputField: E1('#chat-input-field'),
135133
inputFile: E1('#chat-input-file'),
136134
contentDiv: E1('div.content'),
137135
viewConfig: E1('#chat-config'),
138136
viewPreview: E1('#chat-preview'),
139137
previewContent: E1('#chat-preview-content'),
@@ -169,56 +167,23 @@
169167
taking into account single- vs multi-line input. The getter returns
170168
a string and the setter returns this object. */
171169
inputValue: function(){
172170
const e = this.inputElement();
173171
if(arguments.length){
174
- e.value = arguments[0];
172
+ e.innerText = arguments[0];
175173
return this;
176174
}
177
- return e.value;
175
+ return e.innerText;
178176
},
179177
/** Asks the current user input field to take focus. Returns this. */
180178
inputFocus: function(){
181179
this.inputElement().focus();
182180
return this;
183181
},
184182
/** Returns the current message input element. */
185183
inputElement: function(){
186
- return this.e.inputCurrent;
187
- },
188
- /** Toggles between single- and multi-line edit modes. Returns this. */
189
- inputToggleSingleMulti: function(){
190
- const old = this.e.inputCurrent;
191
- if(this.e.inputCurrent === this.e.inputSingle){
192
- this.e.inputCurrent = this.e.inputMulti;
193
- this.e.inputLine.classList.remove('single-line');
194
- }else{
195
- this.e.inputCurrent = this.e.inputSingle;
196
- this.e.inputLine.classList.add('single-line');
197
- }
198
- const m = this.e.viewMessages,
199
- sTop = m.scrollTop,
200
- mh1 = m.clientHeight;
201
- D.addClass(old, 'hidden');
202
- D.removeClass(this.e.inputCurrent, 'hidden');
203
- const mh2 = m.clientHeight;
204
- m.scrollTo(0, sTop + (mh1-mh2));
205
- this.e.inputCurrent.value = old.value;
206
- old.value = '';
207
- return this;
208
- },
209
- /**
210
- If passed true or no arguments, switches to multi-line mode
211
- if currently in single-line mode. If passed false, switches
212
- to single-line mode if currently in multi-line mode. Returns
213
- this.
214
- */
215
- inputMultilineMode: function(yes){
216
- if(!arguments.length) yes = true;
217
- if(yes && this.e.inputCurrent === this.e.inputMulti) return this;
218
- else if(!yes && this.e.inputCurrent === this.e.inputSingle) return this;
219
- else return this.inputToggleSingleMulti();
184
+ return this.e.inputField;
220185
},
221186
/** Enables (if yes is truthy) or disables all elements in
222187
* this.disableDuringAjax. */
223188
enableAjaxComponents: function(yes){
224189
D[yes ? 'enable' : 'disable'](this.disableDuringAjax);
@@ -392,17 +357,25 @@
392357
return e ? overlapsElemView(e, this.e.viewMessages) : false;
393358
},
394359
settings:{
395360
get: (k,dflt)=>F.storage.get(k,dflt),
396361
getBool: (k,dflt)=>F.storage.getBool(k,dflt),
397
- set: (k,v)=>F.storage.set(k,v),
362
+ set: function(k,v){
363
+ F.storage.set(k,v);
364
+ F.page.dispatchEvent('chat-setting',{key: k, value: v});
365
+ },
398366
/* Toggles the boolean setting specified by k. Returns the
399367
new value.*/
400368
toggle: function(k){
401369
const v = this.getBool(k);
402370
this.set(k, !v);
403371
return !v;
372
+ },
373
+ addListener: function(setting, f){
374
+ F.page.addEventListener('chat-setting', function(ev){
375
+ if(ev.detail.key===setting) f(ev.detail);
376
+ }, false);
404377
},
405378
defaults:{
406379
"images-inline": !!F.config.chat.imagesInline,
407380
"edit-multiline": false,
408381
"monospace-messages": false,
@@ -449,11 +422,13 @@
449422
return e;
450423
}
451424
this.e.views.forEach(function(E){
452425
if(e!==E) D.addClass(E,'hidden');
453426
});
454
- this.e.currentView = D.removeClass(e,'hidden');
427
+ this.e.currentView = e;
428
+ if(this.e.currentView.$beforeShow) this.e.currentView.$beforeShow();
429
+ D.removeClass(e,'hidden');
455430
this.animate(this.e.currentView, 'anim-fade-in-fast');
456431
return this.e.currentView;
457432
},
458433
/**
459434
Updates the "active user list" view if we are not currently
@@ -499,10 +474,35 @@
499474
Object.keys(this.usersLastSeen).sort(
500475
callee.sortUsersSeen
501476
).forEach(callee.addUserElem);
502477
return this;
503478
},
479
+ /** Show or hide the active user list. Returns this object. */
480
+ showActiveUserList: function(yes){
481
+ if(0===arguments.length) yes = true;
482
+ this.e.activeUserListWrapper.classList[
483
+ yes ? 'remove' : 'add'
484
+ ]('hidden');
485
+ D.removeClass(Chat.e.activeUserListWrapper, 'collapsed');
486
+ if(Chat.e.activeUserListWrapper.classList.contains('hidden')){
487
+ /* When hiding this element, undo all filtering */
488
+ Chat.setUserFilter(false);
489
+ /*Ideally we'd scroll the final message into view
490
+ now, but because viewMessages is currently hidden behind
491
+ viewConfig, scrolling is a no-op. */
492
+ Chat.scrollMessagesTo(1);
493
+ }else{
494
+ Chat.updateActiveUserList();
495
+ Chat.animate(Chat.e.activeUserListWrapper, 'anim-flip-v');
496
+ }
497
+ return this;
498
+ },
499
+ showActiveUserTimestamps: function(yes){
500
+ if(0===arguments.length) yes = true;
501
+ this.e.activeUserList.classList[yes ? 'add' : 'remove']('timestamps');
502
+ return this;
503
+ },
504504
/**
505505
Applies user name filter to all current messages, or clears
506506
the filter if uname is falsy.
507507
*/
508508
setUserFilter: function(uname){
@@ -545,35 +545,10 @@
545545
}
546546
};
547547
cs.animate.$disabled = true;
548548
F.fetch.beforesend = ()=>cs.ajaxStart();
549549
F.fetch.aftersend = ()=>cs.ajaxEnd();
550
- cs.e.inputCurrent = cs.e.inputSingle;
551
- /* Install default settings... */
552
- Object.keys(cs.settings.defaults).forEach(function(k){
553
- const v = cs.settings.get(k,cs);
554
- if(cs===v) cs.settings.set(k,cs.settings.defaults[k]);
555
- });
556
- if(window.innerWidth<window.innerHeight){
557
- /* Alignment of 'my' messages: right alignment is conventional
558
- for mobile chat apps but can be difficult to read in wide
559
- windows (desktop/tablet landscape mode), so we default to a
560
- layout based on the apparent "orientation" of the window:
561
- tall vs wide. Can be toggled via settings popup. */
562
- document.body.classList.add('my-messages-right');
563
- }
564
- if(cs.settings.getBool('monospace-messages',false)){
565
- document.body.classList.add('monospace-messages');
566
- }
567
- if(cs.settings.getBool('active-user-list',false)){
568
- cs.e.activeUserListWrapper.classList.remove('hidden');
569
- }
570
- if(cs.settings.getBool('active-user-list-timestamps',false)){
571
- cs.e.activeUserList.classList.add('timestamps');
572
- }
573
- cs.inputMultilineMode(cs.settings.getBool('edit-multiline',false));
574
- cs.chatOnlyMode(cs.settings.getBool('chat-only-mode'));
575550
cs.pageTitleOrig = cs.e.pageTitle.innerText;
576551
const qs = (e)=>document.querySelector(e);
577552
const argsToArray = function(args){
578553
return Array.prototype.slice.call(args,0);
579554
};
@@ -1194,47 +1169,59 @@
11941169
});
11951170
BlobXferState.clear();
11961171
Chat.inputValue("").inputFocus();
11971172
};
11981173
1199
- const inputWidgetKeydown = function(ev){
1174
+ const inputWidgetKeydown = function f(ev){
1175
+ if(!f.$toggle){
1176
+ f.$toggle = function(currentMode){
1177
+ currentMode = !currentMode;
1178
+ Chat.settings.set('edit-multiline', currentMode);
1179
+ };
1180
+ }
12001181
if(13 === ev.keyCode){
1182
+ const multi = Chat.settings.getBool('edit-multiline', false);
12011183
if(ev.shiftKey){
12021184
ev.preventDefault();
12031185
ev.stopPropagation();
12041186
/* Shift-enter will run preview mode UNLESS preview mode is
12051187
active AND the input field is empty, in which case it will
12061188
switch back to message view. */
1207
- if(Chat.e.currentView===Chat.e.viewPreview
1208
- && !Chat.e.inputCurrent.value){
1209
- Chat.setCurrentView(Chat.e.viewMessages);
1210
- }else{
1211
- Chat.e.btnPreview.click();
1212
- }
1189
+ const text = Chat.inputValue().trim();
1190
+ if(Chat.e.currentView===Chat.e.viewPreview && !text) Chat.setCurrentView(Chat.e.viewMessages);
1191
+ else if(!text) f.$toggle(multi);
1192
+ else Chat.e.btnPreview.click();
12131193
return false;
1214
- }else if((Chat.e.inputSingle===ev.target)
1215
- || (ev.ctrlKey && Chat.e.inputMulti===ev.target)){
1194
+ }else if(!multi || (ev.ctrlKey && multi)){
12161195
/* ^^^ note that it is intended that both ctrl-enter and enter
12171196
work for single-line input mode. */
12181197
ev.preventDefault();
12191198
ev.stopPropagation();
1220
- Chat.submitMessage();
1199
+ const text = Chat.inputValue().trim();
1200
+ if(!text) f.$toggle(multi);
1201
+ else Chat.submitMessage();
12211202
return false;
12221203
}
12231204
}
12241205
};
1225
- Chat.e.inputSingle
1226
- .addEventListener('keydown', inputWidgetKeydown, false);
1227
- Chat.e.inputMulti
1228
- .addEventListener('keydown', inputWidgetKeydown, false);
1206
+ Chat.e.inputField.addEventListener('keydown', inputWidgetKeydown, false);
12291207
Chat.e.btnSubmit.addEventListener('click',(e)=>{
12301208
e.preventDefault();
12311209
Chat.submitMessage();
12321210
return false;
12331211
});
12341212
1235
- (function(){/*Set up #chat-settings-button */
1213
+ (function(){/*Set up #chat-settings-button and related bits */
1214
+ if(window.innerWidth<window.innerHeight){
1215
+ // Must be set up before config view is...
1216
+ /* Alignment of 'my' messages: right alignment is conventional
1217
+ for mobile chat apps but can be difficult to read in wide
1218
+ windows (desktop/tablet landscape mode), so we default to a
1219
+ layout based on the apparent "orientation" of the window:
1220
+ tall vs wide. Can be toggled via settings. */
1221
+ document.body.classList.add('my-messages-right');
1222
+ }
12361223
const settingsButton = document.querySelector('#chat-settings-button');
12371224
const optionsMenu = E1('#chat-config-options');
12381225
const cbToggle = function(ev){
12391226
ev.preventDefault();
12401227
ev.stopPropagation();
@@ -1248,27 +1235,11 @@
12481235
/** Internal acrobatics to allow certain settings toggles to access
12491236
related toggles. */
12501237
const namedOptions = {
12511238
activeUsers:{
12521239
label: "Show active users list",
1253
- boolValue: ()=>!Chat.e.activeUserListWrapper.classList.contains('hidden'),
1254
- persistentSetting: 'active-user-list',
1255
- callback: function(){
1256
- D.toggleClass(Chat.e.activeUserListWrapper,'hidden');
1257
- D.removeClass(Chat.e.activeUserListWrapper, 'collapsed');
1258
- if(Chat.e.activeUserListWrapper.classList.contains('hidden')){
1259
- /* When hiding this element, undo all filtering */
1260
- Chat.setUserFilter(false);
1261
- /*Ideally we'd scroll the final message into view
1262
- now, but because viewMessages is currently hidden behind
1263
- viewConfig, scrolling is a no-op. */
1264
- Chat.scrollMessagesTo(1);
1265
- }else{
1266
- Chat.updateActiveUserList();
1267
- Chat.animate(Chat.e.activeUserListWrapper, 'anim-flip-v');
1268
- }
1269
- }
1240
+ boolValue: 'active-user-list'
12701241
}
12711242
};
12721243
if(1){
12731244
/* Per user request, toggle the list of users on and off if the
12741245
legend element is tapped. */
@@ -1284,61 +1255,59 @@
12841255
}/*namedOptions.activeUsers additional setup*/
12851256
/* Settings menu entries... Remember that they will be rendered in
12861257
reverse order and the most frequently-needed ones "should"
12871258
(arguably) be closer to the start of this list so that they
12881259
will be rendered within easier reach of the settings button. */
1260
+ /**
1261
+ Settings ops structure:
1262
+
1263
+ label: string for the UI
1264
+
1265
+ boolValue: string (name of Chat.settings setting) or a
1266
+ function which returns true or false.
1267
+
1268
+ select: SELECT element (instead of boolValue)
1269
+
1270
+ callback: optional handler to call after setting is modified.
1271
+
1272
+ If a setting has a boolValue set, that gets transformed into a
1273
+ checkbox which toggles the given persistent setting (if
1274
+ boolValue is a string) AND listens for changes to that setting
1275
+ fired via Chat.settings.set() so that the checkbox can stay in
1276
+ sync with external changes to that setting. Various Chat UI
1277
+ elements stay in sync with the config UI via those settings
1278
+ events.
1279
+ */
12891280
const settingsOps = [{
12901281
label: "Multi-line input",
1291
- boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti,
1292
- persistentSetting: 'edit-multiline',
1293
- callback: function(){
1294
- Chat.inputToggleSingleMulti();
1295
- }
1282
+ boolValue: 'edit-multiline'
12961283
},{
12971284
label: "Left-align my posts",
12981285
boolValue: ()=>!document.body.classList.contains('my-messages-right'),
12991286
callback: function f(){
1300
- document.body.classList.toggle('my-messages-right');
1287
+ document.body.classList[
1288
+ this.checkbox.checked ? 'remove' : 'add'
1289
+ ]('my-messages-right');
13011290
}
13021291
},{
13031292
label: "Show images inline",
1304
- boolValue: ()=>Chat.settings.getBool('images-inline'),
1305
- callback: function(){
1306
- const v = Chat.settings.toggle('images-inline');
1307
- F.toast.message("Image mode set to "+(v ? "inline" : "hyperlink")+".");
1308
- }
1293
+ boolValue: 'images-inline'
13091294
},{
13101295
label: "Timestamps in active users list",
1311
- boolValue: ()=>Chat.e.activeUserList.classList.contains('timestamps'),
1312
- persistentSetting: 'active-user-list-timestamps',
1313
- callback: function(){
1314
- D.toggleClass(Chat.e.activeUserList,'timestamps');
1315
- /* If the timestamp option is activated but
1316
- namedOptions.activeUsers is not currently checked then
1317
- toggle that option on as well. */
1318
- if(Chat.e.activeUserList.classList.contains('timestamps')
1319
- && !namedOptions.activeUsers.boolValue()){
1320
- namedOptions.activeUsers.checkbox.checked = true;
1321
- namedOptions.activeUsers.callback();
1322
- Chat.settings.set(namedOptions.activeUsers.persistentSetting, true);
1323
- }
1324
- }
1296
+ boolValue: 'active-user-list-timestamps'
13251297
},
13261298
namedOptions.activeUsers,{
13271299
label: "Monospace message font",
1328
- boolValue: ()=>document.body.classList.contains('monospace-messages'),
1329
- persistentSetting: 'monospace-messages',
1330
- callback: function(){
1331
- document.body.classList.toggle('monospace-messages');
1300
+ boolValue: 'monospace-messages',
1301
+ callback: function(setting){
1302
+ document.body.classList[
1303
+ setting.value ? 'add' : 'remove'
1304
+ ]('monospace-messages');
13321305
}
13331306
},{
13341307
label: "Chat-only mode",
1335
- boolValue: ()=>Chat.isChatOnlyMode(),
1336
- persistentSetting: 'chat-only-mode',
1337
- callback: function(){
1338
- Chat.toggleChatOnlyMode();
1339
- }
1308
+ boolValue: 'chat-only-mode'
13401309
}];
13411310
13421311
/** Set up selection list of notification sounds. */
13431312
if(1){
13441313
const selectSound = D.select();
@@ -1375,63 +1344,113 @@
13751344
settingsOps.forEach(function f(op){
13761345
const line = D.addClass(D.div(), 'menu-entry');
13771346
const btn = D.append(
13781347
D.addClass(D.label(), 'cbutton'/*bootstrap skin hijacks 'button'*/),
13791348
op.label);
1380
- const callback = function(ev){
1381
- op.callback(ev);
1382
- if(op.persistentSetting){
1383
- Chat.settings.set(op.persistentSetting, op.boolValue());
1384
- }
1385
- };
13861349
if(op.hasOwnProperty('select')){
13871350
D.append(line, btn, op.select);
1388
- op.select.addEventListener('change', callback, false);
1351
+ if(op.callback){
1352
+ op.select.addEventListener('change', (ev)=>op.callback(ev), false);
1353
+ }
13891354
}else if(op.hasOwnProperty('boolValue')){
13901355
if(undefined === f.$id) f.$id = 0;
13911356
++f.$id;
1357
+ if('string' ===typeof op.boolValue){
1358
+ const key = op.boolValue;
1359
+ op.boolValue = ()=>Chat.settings.getBool(key);
1360
+ op.persistentSetting = key;
1361
+ }
13921362
const check = op.checkbox
13931363
= D.attr(D.checkbox(1, op.boolValue()),
13941364
'aria-label', op.label);
13951365
const id = 'cfgopt'+f.$id;
1396
- if(op.boolValue()) check.checked = true;
1366
+ check.checked = op.boolValue();
1367
+ op.checkbox = check;
13971368
D.attr(check, 'id', id);
13981369
D.attr(btn, 'for', id);
13991370
D.append(line, check);
1400
- check.addEventListener('change', callback);
14011371
D.append(line, btn);
14021372
}else{
14031373
line.addEventListener('click', callback);
14041374
D.append(line, btn);
14051375
}
14061376
D.append(optionsMenu, line);
1377
+ if(op.persistentSetting){
1378
+ Chat.settings.addListener(
1379
+ op.persistentSetting,
1380
+ function(setting){
1381
+ if(op.checkbox) op.checkbox.checked = !!setting.value;
1382
+ else if(op.select) op.select.value = setting.value;
1383
+ if(op.callback) op.callback(setting);
1384
+ }
1385
+ );
1386
+ if(op.checkbox){
1387
+ op.checkbox.addEventListener(
1388
+ 'change', function(){
1389
+ Chat.settings.set(op.persistentSetting, op.checkbox.checked)
1390
+ }, false);
1391
+ }
1392
+ }else if(op.callback && op.checkbox){
1393
+ op.checkbox.addEventListener('change', (ev)=>op.callback(ev), false);
1394
+ }
14071395
});
1408
- if(0 && settingsOps.selectSound){
1409
- D.append(optionsMenu, settingsOps.selectSound);
1410
- }
1411
- //settingsButton.click()/*for for development*/;
14121396
})()/*#chat-settings-button setup*/;
14131397
1398
+ (function(){
1399
+ /* Install default settings... must come after
1400
+ chat-settings-button setup so that the listeners which that
1401
+ installs are notified via the properties getting initialized
1402
+ here. */
1403
+ Chat.settings.addListener('monospace-messages',function(s){
1404
+ document.body.classList[s.value ? 'add' : 'remove']('monospace-messages');
1405
+ })
1406
+ Chat.settings.addListener('active-user-list',function(s){
1407
+ Chat.showActiveUserList(s.value);
1408
+ });
1409
+ Chat.settings.addListener('active-user-list-timestamps',function(s){
1410
+ Chat.showActiveUserTimestamps(s.value);
1411
+ });
1412
+ Chat.settings.addListener('chat-only-mode',function(s){
1413
+ Chat.chatOnlyMode(s.value);
1414
+ });
1415
+ Chat.settings.addListener('edit-multiline',function(s){
1416
+ Chat.e.inputLine.classList[
1417
+ s.value ? 'remove' : 'add'
1418
+ ]('single-line');
1419
+ });
1420
+ const valueKludges = {
1421
+ /* Convert certain string-format values to other types... */
1422
+ "false": false,
1423
+ "true": true
1424
+ };
1425
+ Object.keys(Chat.settings.defaults).forEach(function(k){
1426
+ var v = Chat.settings.get(k,Chat);
1427
+ if(Chat===v) v = Chat.settings.defaults[k];
1428
+ if(valueKludges.hasOwnProperty(v)) v = valueKludges[v];
1429
+ Chat.settings.set(k,v)
1430
+ /* fires event listeners so that the Config area checkboxes
1431
+ get in sync */;
1432
+ });
1433
+ })();
1434
+
14141435
(function(){/*set up message preview*/
14151436
const btnPreview = Chat.e.btnPreview;
14161437
Chat.setPreviewText = function(t){
14171438
this.setCurrentView(this.e.viewPreview);
14181439
this.e.previewContent.innerHTML = t;
14191440
this.e.viewPreview.querySelectorAll('a').forEach(addAnchorTargetBlank);
1420
- this.e.inputCurrent.focus();
1441
+ this.inputFocus();
14211442
};
14221443
Chat.e.viewPreview.querySelector('#chat-preview-close').
14231444
addEventListener('click', ()=>Chat.setCurrentView(Chat.e.viewMessages), false);
14241445
let previewPending = false;
1425
- const elemsToEnable = [
1426
- btnPreview, Chat.e.btnSubmit,
1427
- Chat.e.inputSingle, Chat.e.inputMulti];
1446
+ const elemsToEnable = [btnPreview, Chat.e.btnSubmit, Chat.e.inputField];
14281447
const submit = function(ev){
14291448
ev.preventDefault();
14301449
ev.stopPropagation();
14311450
if(previewPending) return false;
1432
- const txt = Chat.e.inputCurrent.value;
1451
+ const txt = Chat.inputValue();
14331452
if(!txt){
14341453
Chat.setPreviewText('');
14351454
previewPending = false;
14361455
return false;
14371456
}
14381457
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -127,13 +127,11 @@
127 inputWrapper: E1("#chat-input-area"),
128 inputLine: E1('#chat-input-line'),
129 fileSelectWrapper: E1('#chat-input-file-area'),
130 viewMessages: E1('#chat-messages-wrapper'),
131 btnSubmit: E1('#chat-message-submit'),
132 inputSingle: E1('#chat-input-single'),
133 inputMulti: E1('#chat-input-multi'),
134 inputCurrent: undefined/*one of inputSingle or inputMulti*/,
135 inputFile: E1('#chat-input-file'),
136 contentDiv: E1('div.content'),
137 viewConfig: E1('#chat-config'),
138 viewPreview: E1('#chat-preview'),
139 previewContent: E1('#chat-preview-content'),
@@ -169,56 +167,23 @@
169 taking into account single- vs multi-line input. The getter returns
170 a string and the setter returns this object. */
171 inputValue: function(){
172 const e = this.inputElement();
173 if(arguments.length){
174 e.value = arguments[0];
175 return this;
176 }
177 return e.value;
178 },
179 /** Asks the current user input field to take focus. Returns this. */
180 inputFocus: function(){
181 this.inputElement().focus();
182 return this;
183 },
184 /** Returns the current message input element. */
185 inputElement: function(){
186 return this.e.inputCurrent;
187 },
188 /** Toggles between single- and multi-line edit modes. Returns this. */
189 inputToggleSingleMulti: function(){
190 const old = this.e.inputCurrent;
191 if(this.e.inputCurrent === this.e.inputSingle){
192 this.e.inputCurrent = this.e.inputMulti;
193 this.e.inputLine.classList.remove('single-line');
194 }else{
195 this.e.inputCurrent = this.e.inputSingle;
196 this.e.inputLine.classList.add('single-line');
197 }
198 const m = this.e.viewMessages,
199 sTop = m.scrollTop,
200 mh1 = m.clientHeight;
201 D.addClass(old, 'hidden');
202 D.removeClass(this.e.inputCurrent, 'hidden');
203 const mh2 = m.clientHeight;
204 m.scrollTo(0, sTop + (mh1-mh2));
205 this.e.inputCurrent.value = old.value;
206 old.value = '';
207 return this;
208 },
209 /**
210 If passed true or no arguments, switches to multi-line mode
211 if currently in single-line mode. If passed false, switches
212 to single-line mode if currently in multi-line mode. Returns
213 this.
214 */
215 inputMultilineMode: function(yes){
216 if(!arguments.length) yes = true;
217 if(yes && this.e.inputCurrent === this.e.inputMulti) return this;
218 else if(!yes && this.e.inputCurrent === this.e.inputSingle) return this;
219 else return this.inputToggleSingleMulti();
220 },
221 /** Enables (if yes is truthy) or disables all elements in
222 * this.disableDuringAjax. */
223 enableAjaxComponents: function(yes){
224 D[yes ? 'enable' : 'disable'](this.disableDuringAjax);
@@ -392,17 +357,25 @@
392 return e ? overlapsElemView(e, this.e.viewMessages) : false;
393 },
394 settings:{
395 get: (k,dflt)=>F.storage.get(k,dflt),
396 getBool: (k,dflt)=>F.storage.getBool(k,dflt),
397 set: (k,v)=>F.storage.set(k,v),
 
 
 
398 /* Toggles the boolean setting specified by k. Returns the
399 new value.*/
400 toggle: function(k){
401 const v = this.getBool(k);
402 this.set(k, !v);
403 return !v;
 
 
 
 
 
404 },
405 defaults:{
406 "images-inline": !!F.config.chat.imagesInline,
407 "edit-multiline": false,
408 "monospace-messages": false,
@@ -449,11 +422,13 @@
449 return e;
450 }
451 this.e.views.forEach(function(E){
452 if(e!==E) D.addClass(E,'hidden');
453 });
454 this.e.currentView = D.removeClass(e,'hidden');
 
 
455 this.animate(this.e.currentView, 'anim-fade-in-fast');
456 return this.e.currentView;
457 },
458 /**
459 Updates the "active user list" view if we are not currently
@@ -499,10 +474,35 @@
499 Object.keys(this.usersLastSeen).sort(
500 callee.sortUsersSeen
501 ).forEach(callee.addUserElem);
502 return this;
503 },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
504 /**
505 Applies user name filter to all current messages, or clears
506 the filter if uname is falsy.
507 */
508 setUserFilter: function(uname){
@@ -545,35 +545,10 @@
545 }
546 };
547 cs.animate.$disabled = true;
548 F.fetch.beforesend = ()=>cs.ajaxStart();
549 F.fetch.aftersend = ()=>cs.ajaxEnd();
550 cs.e.inputCurrent = cs.e.inputSingle;
551 /* Install default settings... */
552 Object.keys(cs.settings.defaults).forEach(function(k){
553 const v = cs.settings.get(k,cs);
554 if(cs===v) cs.settings.set(k,cs.settings.defaults[k]);
555 });
556 if(window.innerWidth<window.innerHeight){
557 /* Alignment of 'my' messages: right alignment is conventional
558 for mobile chat apps but can be difficult to read in wide
559 windows (desktop/tablet landscape mode), so we default to a
560 layout based on the apparent "orientation" of the window:
561 tall vs wide. Can be toggled via settings popup. */
562 document.body.classList.add('my-messages-right');
563 }
564 if(cs.settings.getBool('monospace-messages',false)){
565 document.body.classList.add('monospace-messages');
566 }
567 if(cs.settings.getBool('active-user-list',false)){
568 cs.e.activeUserListWrapper.classList.remove('hidden');
569 }
570 if(cs.settings.getBool('active-user-list-timestamps',false)){
571 cs.e.activeUserList.classList.add('timestamps');
572 }
573 cs.inputMultilineMode(cs.settings.getBool('edit-multiline',false));
574 cs.chatOnlyMode(cs.settings.getBool('chat-only-mode'));
575 cs.pageTitleOrig = cs.e.pageTitle.innerText;
576 const qs = (e)=>document.querySelector(e);
577 const argsToArray = function(args){
578 return Array.prototype.slice.call(args,0);
579 };
@@ -1194,47 +1169,59 @@
1194 });
1195 BlobXferState.clear();
1196 Chat.inputValue("").inputFocus();
1197 };
1198
1199 const inputWidgetKeydown = function(ev){
 
 
 
 
 
 
1200 if(13 === ev.keyCode){
 
1201 if(ev.shiftKey){
1202 ev.preventDefault();
1203 ev.stopPropagation();
1204 /* Shift-enter will run preview mode UNLESS preview mode is
1205 active AND the input field is empty, in which case it will
1206 switch back to message view. */
1207 if(Chat.e.currentView===Chat.e.viewPreview
1208 && !Chat.e.inputCurrent.value){
1209 Chat.setCurrentView(Chat.e.viewMessages);
1210 }else{
1211 Chat.e.btnPreview.click();
1212 }
1213 return false;
1214 }else if((Chat.e.inputSingle===ev.target)
1215 || (ev.ctrlKey && Chat.e.inputMulti===ev.target)){
1216 /* ^^^ note that it is intended that both ctrl-enter and enter
1217 work for single-line input mode. */
1218 ev.preventDefault();
1219 ev.stopPropagation();
1220 Chat.submitMessage();
 
 
1221 return false;
1222 }
1223 }
1224 };
1225 Chat.e.inputSingle
1226 .addEventListener('keydown', inputWidgetKeydown, false);
1227 Chat.e.inputMulti
1228 .addEventListener('keydown', inputWidgetKeydown, false);
1229 Chat.e.btnSubmit.addEventListener('click',(e)=>{
1230 e.preventDefault();
1231 Chat.submitMessage();
1232 return false;
1233 });
1234
1235 (function(){/*Set up #chat-settings-button */
 
 
 
 
 
 
 
 
 
1236 const settingsButton = document.querySelector('#chat-settings-button');
1237 const optionsMenu = E1('#chat-config-options');
1238 const cbToggle = function(ev){
1239 ev.preventDefault();
1240 ev.stopPropagation();
@@ -1248,27 +1235,11 @@
1248 /** Internal acrobatics to allow certain settings toggles to access
1249 related toggles. */
1250 const namedOptions = {
1251 activeUsers:{
1252 label: "Show active users list",
1253 boolValue: ()=>!Chat.e.activeUserListWrapper.classList.contains('hidden'),
1254 persistentSetting: 'active-user-list',
1255 callback: function(){
1256 D.toggleClass(Chat.e.activeUserListWrapper,'hidden');
1257 D.removeClass(Chat.e.activeUserListWrapper, 'collapsed');
1258 if(Chat.e.activeUserListWrapper.classList.contains('hidden')){
1259 /* When hiding this element, undo all filtering */
1260 Chat.setUserFilter(false);
1261 /*Ideally we'd scroll the final message into view
1262 now, but because viewMessages is currently hidden behind
1263 viewConfig, scrolling is a no-op. */
1264 Chat.scrollMessagesTo(1);
1265 }else{
1266 Chat.updateActiveUserList();
1267 Chat.animate(Chat.e.activeUserListWrapper, 'anim-flip-v');
1268 }
1269 }
1270 }
1271 };
1272 if(1){
1273 /* Per user request, toggle the list of users on and off if the
1274 legend element is tapped. */
@@ -1284,61 +1255,59 @@
1284 }/*namedOptions.activeUsers additional setup*/
1285 /* Settings menu entries... Remember that they will be rendered in
1286 reverse order and the most frequently-needed ones "should"
1287 (arguably) be closer to the start of this list so that they
1288 will be rendered within easier reach of the settings button. */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1289 const settingsOps = [{
1290 label: "Multi-line input",
1291 boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti,
1292 persistentSetting: 'edit-multiline',
1293 callback: function(){
1294 Chat.inputToggleSingleMulti();
1295 }
1296 },{
1297 label: "Left-align my posts",
1298 boolValue: ()=>!document.body.classList.contains('my-messages-right'),
1299 callback: function f(){
1300 document.body.classList.toggle('my-messages-right');
 
 
1301 }
1302 },{
1303 label: "Show images inline",
1304 boolValue: ()=>Chat.settings.getBool('images-inline'),
1305 callback: function(){
1306 const v = Chat.settings.toggle('images-inline');
1307 F.toast.message("Image mode set to "+(v ? "inline" : "hyperlink")+".");
1308 }
1309 },{
1310 label: "Timestamps in active users list",
1311 boolValue: ()=>Chat.e.activeUserList.classList.contains('timestamps'),
1312 persistentSetting: 'active-user-list-timestamps',
1313 callback: function(){
1314 D.toggleClass(Chat.e.activeUserList,'timestamps');
1315 /* If the timestamp option is activated but
1316 namedOptions.activeUsers is not currently checked then
1317 toggle that option on as well. */
1318 if(Chat.e.activeUserList.classList.contains('timestamps')
1319 && !namedOptions.activeUsers.boolValue()){
1320 namedOptions.activeUsers.checkbox.checked = true;
1321 namedOptions.activeUsers.callback();
1322 Chat.settings.set(namedOptions.activeUsers.persistentSetting, true);
1323 }
1324 }
1325 },
1326 namedOptions.activeUsers,{
1327 label: "Monospace message font",
1328 boolValue: ()=>document.body.classList.contains('monospace-messages'),
1329 persistentSetting: 'monospace-messages',
1330 callback: function(){
1331 document.body.classList.toggle('monospace-messages');
 
1332 }
1333 },{
1334 label: "Chat-only mode",
1335 boolValue: ()=>Chat.isChatOnlyMode(),
1336 persistentSetting: 'chat-only-mode',
1337 callback: function(){
1338 Chat.toggleChatOnlyMode();
1339 }
1340 }];
1341
1342 /** Set up selection list of notification sounds. */
1343 if(1){
1344 const selectSound = D.select();
@@ -1375,63 +1344,113 @@
1375 settingsOps.forEach(function f(op){
1376 const line = D.addClass(D.div(), 'menu-entry');
1377 const btn = D.append(
1378 D.addClass(D.label(), 'cbutton'/*bootstrap skin hijacks 'button'*/),
1379 op.label);
1380 const callback = function(ev){
1381 op.callback(ev);
1382 if(op.persistentSetting){
1383 Chat.settings.set(op.persistentSetting, op.boolValue());
1384 }
1385 };
1386 if(op.hasOwnProperty('select')){
1387 D.append(line, btn, op.select);
1388 op.select.addEventListener('change', callback, false);
 
 
1389 }else if(op.hasOwnProperty('boolValue')){
1390 if(undefined === f.$id) f.$id = 0;
1391 ++f.$id;
 
 
 
 
 
1392 const check = op.checkbox
1393 = D.attr(D.checkbox(1, op.boolValue()),
1394 'aria-label', op.label);
1395 const id = 'cfgopt'+f.$id;
1396 if(op.boolValue()) check.checked = true;
 
1397 D.attr(check, 'id', id);
1398 D.attr(btn, 'for', id);
1399 D.append(line, check);
1400 check.addEventListener('change', callback);
1401 D.append(line, btn);
1402 }else{
1403 line.addEventListener('click', callback);
1404 D.append(line, btn);
1405 }
1406 D.append(optionsMenu, line);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1407 });
1408 if(0 && settingsOps.selectSound){
1409 D.append(optionsMenu, settingsOps.selectSound);
1410 }
1411 //settingsButton.click()/*for for development*/;
1412 })()/*#chat-settings-button setup*/;
1413
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1414 (function(){/*set up message preview*/
1415 const btnPreview = Chat.e.btnPreview;
1416 Chat.setPreviewText = function(t){
1417 this.setCurrentView(this.e.viewPreview);
1418 this.e.previewContent.innerHTML = t;
1419 this.e.viewPreview.querySelectorAll('a').forEach(addAnchorTargetBlank);
1420 this.e.inputCurrent.focus();
1421 };
1422 Chat.e.viewPreview.querySelector('#chat-preview-close').
1423 addEventListener('click', ()=>Chat.setCurrentView(Chat.e.viewMessages), false);
1424 let previewPending = false;
1425 const elemsToEnable = [
1426 btnPreview, Chat.e.btnSubmit,
1427 Chat.e.inputSingle, Chat.e.inputMulti];
1428 const submit = function(ev){
1429 ev.preventDefault();
1430 ev.stopPropagation();
1431 if(previewPending) return false;
1432 const txt = Chat.e.inputCurrent.value;
1433 if(!txt){
1434 Chat.setPreviewText('');
1435 previewPending = false;
1436 return false;
1437 }
1438
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -127,13 +127,11 @@
127 inputWrapper: E1("#chat-input-area"),
128 inputLine: E1('#chat-input-line'),
129 fileSelectWrapper: E1('#chat-input-file-area'),
130 viewMessages: E1('#chat-messages-wrapper'),
131 btnSubmit: E1('#chat-message-submit'),
132 inputField: E1('#chat-input-field'),
 
 
133 inputFile: E1('#chat-input-file'),
134 contentDiv: E1('div.content'),
135 viewConfig: E1('#chat-config'),
136 viewPreview: E1('#chat-preview'),
137 previewContent: E1('#chat-preview-content'),
@@ -169,56 +167,23 @@
167 taking into account single- vs multi-line input. The getter returns
168 a string and the setter returns this object. */
169 inputValue: function(){
170 const e = this.inputElement();
171 if(arguments.length){
172 e.innerText = arguments[0];
173 return this;
174 }
175 return e.innerText;
176 },
177 /** Asks the current user input field to take focus. Returns this. */
178 inputFocus: function(){
179 this.inputElement().focus();
180 return this;
181 },
182 /** Returns the current message input element. */
183 inputElement: function(){
184 return this.e.inputField;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185 },
186 /** Enables (if yes is truthy) or disables all elements in
187 * this.disableDuringAjax. */
188 enableAjaxComponents: function(yes){
189 D[yes ? 'enable' : 'disable'](this.disableDuringAjax);
@@ -392,17 +357,25 @@
357 return e ? overlapsElemView(e, this.e.viewMessages) : false;
358 },
359 settings:{
360 get: (k,dflt)=>F.storage.get(k,dflt),
361 getBool: (k,dflt)=>F.storage.getBool(k,dflt),
362 set: function(k,v){
363 F.storage.set(k,v);
364 F.page.dispatchEvent('chat-setting',{key: k, value: v});
365 },
366 /* Toggles the boolean setting specified by k. Returns the
367 new value.*/
368 toggle: function(k){
369 const v = this.getBool(k);
370 this.set(k, !v);
371 return !v;
372 },
373 addListener: function(setting, f){
374 F.page.addEventListener('chat-setting', function(ev){
375 if(ev.detail.key===setting) f(ev.detail);
376 }, false);
377 },
378 defaults:{
379 "images-inline": !!F.config.chat.imagesInline,
380 "edit-multiline": false,
381 "monospace-messages": false,
@@ -449,11 +422,13 @@
422 return e;
423 }
424 this.e.views.forEach(function(E){
425 if(e!==E) D.addClass(E,'hidden');
426 });
427 this.e.currentView = e;
428 if(this.e.currentView.$beforeShow) this.e.currentView.$beforeShow();
429 D.removeClass(e,'hidden');
430 this.animate(this.e.currentView, 'anim-fade-in-fast');
431 return this.e.currentView;
432 },
433 /**
434 Updates the "active user list" view if we are not currently
@@ -499,10 +474,35 @@
474 Object.keys(this.usersLastSeen).sort(
475 callee.sortUsersSeen
476 ).forEach(callee.addUserElem);
477 return this;
478 },
479 /** Show or hide the active user list. Returns this object. */
480 showActiveUserList: function(yes){
481 if(0===arguments.length) yes = true;
482 this.e.activeUserListWrapper.classList[
483 yes ? 'remove' : 'add'
484 ]('hidden');
485 D.removeClass(Chat.e.activeUserListWrapper, 'collapsed');
486 if(Chat.e.activeUserListWrapper.classList.contains('hidden')){
487 /* When hiding this element, undo all filtering */
488 Chat.setUserFilter(false);
489 /*Ideally we'd scroll the final message into view
490 now, but because viewMessages is currently hidden behind
491 viewConfig, scrolling is a no-op. */
492 Chat.scrollMessagesTo(1);
493 }else{
494 Chat.updateActiveUserList();
495 Chat.animate(Chat.e.activeUserListWrapper, 'anim-flip-v');
496 }
497 return this;
498 },
499 showActiveUserTimestamps: function(yes){
500 if(0===arguments.length) yes = true;
501 this.e.activeUserList.classList[yes ? 'add' : 'remove']('timestamps');
502 return this;
503 },
504 /**
505 Applies user name filter to all current messages, or clears
506 the filter if uname is falsy.
507 */
508 setUserFilter: function(uname){
@@ -545,35 +545,10 @@
545 }
546 };
547 cs.animate.$disabled = true;
548 F.fetch.beforesend = ()=>cs.ajaxStart();
549 F.fetch.aftersend = ()=>cs.ajaxEnd();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
550 cs.pageTitleOrig = cs.e.pageTitle.innerText;
551 const qs = (e)=>document.querySelector(e);
552 const argsToArray = function(args){
553 return Array.prototype.slice.call(args,0);
554 };
@@ -1194,47 +1169,59 @@
1169 });
1170 BlobXferState.clear();
1171 Chat.inputValue("").inputFocus();
1172 };
1173
1174 const inputWidgetKeydown = function f(ev){
1175 if(!f.$toggle){
1176 f.$toggle = function(currentMode){
1177 currentMode = !currentMode;
1178 Chat.settings.set('edit-multiline', currentMode);
1179 };
1180 }
1181 if(13 === ev.keyCode){
1182 const multi = Chat.settings.getBool('edit-multiline', false);
1183 if(ev.shiftKey){
1184 ev.preventDefault();
1185 ev.stopPropagation();
1186 /* Shift-enter will run preview mode UNLESS preview mode is
1187 active AND the input field is empty, in which case it will
1188 switch back to message view. */
1189 const text = Chat.inputValue().trim();
1190 if(Chat.e.currentView===Chat.e.viewPreview && !text) Chat.setCurrentView(Chat.e.viewMessages);
1191 else if(!text) f.$toggle(multi);
1192 else Chat.e.btnPreview.click();
 
 
1193 return false;
1194 }else if(!multi || (ev.ctrlKey && multi)){
 
1195 /* ^^^ note that it is intended that both ctrl-enter and enter
1196 work for single-line input mode. */
1197 ev.preventDefault();
1198 ev.stopPropagation();
1199 const text = Chat.inputValue().trim();
1200 if(!text) f.$toggle(multi);
1201 else Chat.submitMessage();
1202 return false;
1203 }
1204 }
1205 };
1206 Chat.e.inputField.addEventListener('keydown', inputWidgetKeydown, false);
 
 
 
1207 Chat.e.btnSubmit.addEventListener('click',(e)=>{
1208 e.preventDefault();
1209 Chat.submitMessage();
1210 return false;
1211 });
1212
1213 (function(){/*Set up #chat-settings-button and related bits */
1214 if(window.innerWidth<window.innerHeight){
1215 // Must be set up before config view is...
1216 /* Alignment of 'my' messages: right alignment is conventional
1217 for mobile chat apps but can be difficult to read in wide
1218 windows (desktop/tablet landscape mode), so we default to a
1219 layout based on the apparent "orientation" of the window:
1220 tall vs wide. Can be toggled via settings. */
1221 document.body.classList.add('my-messages-right');
1222 }
1223 const settingsButton = document.querySelector('#chat-settings-button');
1224 const optionsMenu = E1('#chat-config-options');
1225 const cbToggle = function(ev){
1226 ev.preventDefault();
1227 ev.stopPropagation();
@@ -1248,27 +1235,11 @@
1235 /** Internal acrobatics to allow certain settings toggles to access
1236 related toggles. */
1237 const namedOptions = {
1238 activeUsers:{
1239 label: "Show active users list",
1240 boolValue: 'active-user-list'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1241 }
1242 };
1243 if(1){
1244 /* Per user request, toggle the list of users on and off if the
1245 legend element is tapped. */
@@ -1284,61 +1255,59 @@
1255 }/*namedOptions.activeUsers additional setup*/
1256 /* Settings menu entries... Remember that they will be rendered in
1257 reverse order and the most frequently-needed ones "should"
1258 (arguably) be closer to the start of this list so that they
1259 will be rendered within easier reach of the settings button. */
1260 /**
1261 Settings ops structure:
1262
1263 label: string for the UI
1264
1265 boolValue: string (name of Chat.settings setting) or a
1266 function which returns true or false.
1267
1268 select: SELECT element (instead of boolValue)
1269
1270 callback: optional handler to call after setting is modified.
1271
1272 If a setting has a boolValue set, that gets transformed into a
1273 checkbox which toggles the given persistent setting (if
1274 boolValue is a string) AND listens for changes to that setting
1275 fired via Chat.settings.set() so that the checkbox can stay in
1276 sync with external changes to that setting. Various Chat UI
1277 elements stay in sync with the config UI via those settings
1278 events.
1279 */
1280 const settingsOps = [{
1281 label: "Multi-line input",
1282 boolValue: 'edit-multiline'
 
 
 
 
1283 },{
1284 label: "Left-align my posts",
1285 boolValue: ()=>!document.body.classList.contains('my-messages-right'),
1286 callback: function f(){
1287 document.body.classList[
1288 this.checkbox.checked ? 'remove' : 'add'
1289 ]('my-messages-right');
1290 }
1291 },{
1292 label: "Show images inline",
1293 boolValue: 'images-inline'
 
 
 
 
1294 },{
1295 label: "Timestamps in active users list",
1296 boolValue: 'active-user-list-timestamps'
 
 
 
 
 
 
 
 
 
 
 
 
 
1297 },
1298 namedOptions.activeUsers,{
1299 label: "Monospace message font",
1300 boolValue: 'monospace-messages',
1301 callback: function(setting){
1302 document.body.classList[
1303 setting.value ? 'add' : 'remove'
1304 ]('monospace-messages');
1305 }
1306 },{
1307 label: "Chat-only mode",
1308 boolValue: 'chat-only-mode'
 
 
 
 
1309 }];
1310
1311 /** Set up selection list of notification sounds. */
1312 if(1){
1313 const selectSound = D.select();
@@ -1375,63 +1344,113 @@
1344 settingsOps.forEach(function f(op){
1345 const line = D.addClass(D.div(), 'menu-entry');
1346 const btn = D.append(
1347 D.addClass(D.label(), 'cbutton'/*bootstrap skin hijacks 'button'*/),
1348 op.label);
 
 
 
 
 
 
1349 if(op.hasOwnProperty('select')){
1350 D.append(line, btn, op.select);
1351 if(op.callback){
1352 op.select.addEventListener('change', (ev)=>op.callback(ev), false);
1353 }
1354 }else if(op.hasOwnProperty('boolValue')){
1355 if(undefined === f.$id) f.$id = 0;
1356 ++f.$id;
1357 if('string' ===typeof op.boolValue){
1358 const key = op.boolValue;
1359 op.boolValue = ()=>Chat.settings.getBool(key);
1360 op.persistentSetting = key;
1361 }
1362 const check = op.checkbox
1363 = D.attr(D.checkbox(1, op.boolValue()),
1364 'aria-label', op.label);
1365 const id = 'cfgopt'+f.$id;
1366 check.checked = op.boolValue();
1367 op.checkbox = check;
1368 D.attr(check, 'id', id);
1369 D.attr(btn, 'for', id);
1370 D.append(line, check);
 
1371 D.append(line, btn);
1372 }else{
1373 line.addEventListener('click', callback);
1374 D.append(line, btn);
1375 }
1376 D.append(optionsMenu, line);
1377 if(op.persistentSetting){
1378 Chat.settings.addListener(
1379 op.persistentSetting,
1380 function(setting){
1381 if(op.checkbox) op.checkbox.checked = !!setting.value;
1382 else if(op.select) op.select.value = setting.value;
1383 if(op.callback) op.callback(setting);
1384 }
1385 );
1386 if(op.checkbox){
1387 op.checkbox.addEventListener(
1388 'change', function(){
1389 Chat.settings.set(op.persistentSetting, op.checkbox.checked)
1390 }, false);
1391 }
1392 }else if(op.callback && op.checkbox){
1393 op.checkbox.addEventListener('change', (ev)=>op.callback(ev), false);
1394 }
1395 });
 
 
 
 
1396 })()/*#chat-settings-button setup*/;
1397
1398 (function(){
1399 /* Install default settings... must come after
1400 chat-settings-button setup so that the listeners which that
1401 installs are notified via the properties getting initialized
1402 here. */
1403 Chat.settings.addListener('monospace-messages',function(s){
1404 document.body.classList[s.value ? 'add' : 'remove']('monospace-messages');
1405 })
1406 Chat.settings.addListener('active-user-list',function(s){
1407 Chat.showActiveUserList(s.value);
1408 });
1409 Chat.settings.addListener('active-user-list-timestamps',function(s){
1410 Chat.showActiveUserTimestamps(s.value);
1411 });
1412 Chat.settings.addListener('chat-only-mode',function(s){
1413 Chat.chatOnlyMode(s.value);
1414 });
1415 Chat.settings.addListener('edit-multiline',function(s){
1416 Chat.e.inputLine.classList[
1417 s.value ? 'remove' : 'add'
1418 ]('single-line');
1419 });
1420 const valueKludges = {
1421 /* Convert certain string-format values to other types... */
1422 "false": false,
1423 "true": true
1424 };
1425 Object.keys(Chat.settings.defaults).forEach(function(k){
1426 var v = Chat.settings.get(k,Chat);
1427 if(Chat===v) v = Chat.settings.defaults[k];
1428 if(valueKludges.hasOwnProperty(v)) v = valueKludges[v];
1429 Chat.settings.set(k,v)
1430 /* fires event listeners so that the Config area checkboxes
1431 get in sync */;
1432 });
1433 })();
1434
1435 (function(){/*set up message preview*/
1436 const btnPreview = Chat.e.btnPreview;
1437 Chat.setPreviewText = function(t){
1438 this.setCurrentView(this.e.viewPreview);
1439 this.e.previewContent.innerHTML = t;
1440 this.e.viewPreview.querySelectorAll('a').forEach(addAnchorTargetBlank);
1441 this.inputFocus();
1442 };
1443 Chat.e.viewPreview.querySelector('#chat-preview-close').
1444 addEventListener('click', ()=>Chat.setCurrentView(Chat.e.viewMessages), false);
1445 let previewPending = false;
1446 const elemsToEnable = [btnPreview, Chat.e.btnSubmit, Chat.e.inputField];
 
 
1447 const submit = function(ev){
1448 ev.preventDefault();
1449 ev.stopPropagation();
1450 if(previewPending) return false;
1451 const txt = Chat.inputValue();
1452 if(!txt){
1453 Chat.setPreviewText('');
1454 previewPending = false;
1455 return false;
1456 }
1457
--- src/style.chat.css
+++ src/style.chat.css
@@ -168,28 +168,31 @@
168168
body.chat #chat-input-area {
169169
display: flex;
170170
flex-direction: column;
171171
padding: 0;
172172
margin: 0;
173
- position: initial /*sticky currently disabled due to scrolling-related issues*/;
174
- /*bottom: 0;*/
173
+ flex: 0 1 auto;
175174
}
176175
body.chat:not(.chat-only-mode) #chat-input-area{
177176
/* Safari user reports that 2em is necessary to keep the file selection
178177
widget from overlapping the page footer, whereas a margin of 0 is fine
179178
for FF/Chrome (and 2em is a *huge* waste of space for those). */
180179
margin-bottom: 0;
181180
}
181
+#chat-input-field {
182
+ padding: 0.2em;
183
+ flex: 20 1 auto;
184
+ border-width: 1px;
185
+ border-style: solid;
186
+}
182187
183188
/* Widget holding the chat message input field, send button, and
184189
settings button. */
185190
body.chat #chat-input-line {
186191
display: flex;
187192
flex-direction: row;
188193
align-items: stretch;
189
-}
190
-body.chat #chat-input-line.single-line {
191194
flex-wrap: wrap;
192195
}
193196
body.chat #chat-edit-buttons {
194197
flex: 1 1 auto;
195198
display: flex;
@@ -205,10 +208,19 @@
205208
}
206209
body.chat #chat-input-line:not(.single-line) #chat-edit-buttons > * {
207210
max-width: 4em;
208211
margin: 0.25em;
209212
}
213
+body.chat #chat-input-line:not(.single-line) #chat-input-field {
214
+ border-left-style: double;
215
+ border-left-width: 3px;
216
+ border-right-style: double;
217
+ border-right-width: 3px;
218
+ max-height: 10em/*arbitrary!*/;
219
+ overflow: auto;
220
+}
221
+
210222
body.chat #chat-input-line.single-line #chat-edit-buttons > * {
211223
margin: 0 0.25em;
212224
}
213225
214226
body.chat #chat-input-line > button {
215227
--- src/style.chat.css
+++ src/style.chat.css
@@ -168,28 +168,31 @@
168 body.chat #chat-input-area {
169 display: flex;
170 flex-direction: column;
171 padding: 0;
172 margin: 0;
173 position: initial /*sticky currently disabled due to scrolling-related issues*/;
174 /*bottom: 0;*/
175 }
176 body.chat:not(.chat-only-mode) #chat-input-area{
177 /* Safari user reports that 2em is necessary to keep the file selection
178 widget from overlapping the page footer, whereas a margin of 0 is fine
179 for FF/Chrome (and 2em is a *huge* waste of space for those). */
180 margin-bottom: 0;
181 }
 
 
 
 
 
 
182
183 /* Widget holding the chat message input field, send button, and
184 settings button. */
185 body.chat #chat-input-line {
186 display: flex;
187 flex-direction: row;
188 align-items: stretch;
189 }
190 body.chat #chat-input-line.single-line {
191 flex-wrap: wrap;
192 }
193 body.chat #chat-edit-buttons {
194 flex: 1 1 auto;
195 display: flex;
@@ -205,10 +208,19 @@
205 }
206 body.chat #chat-input-line:not(.single-line) #chat-edit-buttons > * {
207 max-width: 4em;
208 margin: 0.25em;
209 }
 
 
 
 
 
 
 
 
 
210 body.chat #chat-input-line.single-line #chat-edit-buttons > * {
211 margin: 0 0.25em;
212 }
213
214 body.chat #chat-input-line > button {
215
--- src/style.chat.css
+++ src/style.chat.css
@@ -168,28 +168,31 @@
168 body.chat #chat-input-area {
169 display: flex;
170 flex-direction: column;
171 padding: 0;
172 margin: 0;
173 flex: 0 1 auto;
 
174 }
175 body.chat:not(.chat-only-mode) #chat-input-area{
176 /* Safari user reports that 2em is necessary to keep the file selection
177 widget from overlapping the page footer, whereas a margin of 0 is fine
178 for FF/Chrome (and 2em is a *huge* waste of space for those). */
179 margin-bottom: 0;
180 }
181 #chat-input-field {
182 padding: 0.2em;
183 flex: 20 1 auto;
184 border-width: 1px;
185 border-style: solid;
186 }
187
188 /* Widget holding the chat message input field, send button, and
189 settings button. */
190 body.chat #chat-input-line {
191 display: flex;
192 flex-direction: row;
193 align-items: stretch;
 
 
194 flex-wrap: wrap;
195 }
196 body.chat #chat-edit-buttons {
197 flex: 1 1 auto;
198 display: flex;
@@ -205,10 +208,19 @@
208 }
209 body.chat #chat-input-line:not(.single-line) #chat-edit-buttons > * {
210 max-width: 4em;
211 margin: 0.25em;
212 }
213 body.chat #chat-input-line:not(.single-line) #chat-input-field {
214 border-left-style: double;
215 border-left-width: 3px;
216 border-right-style: double;
217 border-right-width: 3px;
218 max-height: 10em/*arbitrary!*/;
219 overflow: auto;
220 }
221
222 body.chat #chat-input-line.single-line #chat-edit-buttons > * {
223 margin: 0 0.25em;
224 }
225
226 body.chat #chat-input-line > button {
227

Keyboard Shortcuts

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