Fossil SCM

When stripping trailing spaces from lines to avoid the "console paste problem", leave markdown paragraph continuation markers intact. Robustified the config view layout in a trial-and-error attempt to defend against Safari-on-iPhone layout bugs.

stephan 2021-09-30 15:59 chat-input-rework
Commit cbc7f117e6292f01ad1e5dbca6d78dfef6522d685c469d93fe2692f0fc3256c2
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -1045,11 +1045,11 @@
10451045
};
10461046
/** Updates the paste/drop zone with details of the pasted/dropped
10471047
data. The argument must be a Blob or Blob-like object (File) or
10481048
it can be falsy to reset/clear that state.*/
10491049
const updateDropZoneContent = function(blob){
1050
- console.debug("updateDropZoneContent()",blob);
1050
+ //console.debug("updateDropZoneContent()",blob);
10511051
const dd = bxs.dropDetails;
10521052
bxs.blob = blob;
10531053
D.clearElement(dd);
10541054
if(!blob){
10551055
Chat.e.inputFile.value = '';
@@ -1152,21 +1152,28 @@
11521152
empty, this is a no-op.
11531153
*/
11541154
Chat.submitMessage = function f(){
11551155
if(!f.spaces){
11561156
f.spaces = /\s+$/;
1157
+ f.markdownContinuation = /\\\s\s$/;
11571158
}
11581159
this.setCurrentView(this.e.viewMessages);
11591160
const fd = new FormData();
11601161
var msg = this.inputValue().trim();
11611162
if(msg && (msg.indexOf('\n')>0 || f.spaces.test(msg))){
11621163
/* Cosmetic: trim whitespace from the ends of lines to try to
11631164
keep copy/paste from terminals, especially wide ones, from
1164
- forcing a horizontal scrollbar on all clients. */
1165
+ forcing a horizontal scrollbar on all clients. This breaks
1166
+ markdown's use of blackslash-space-space for paragraph
1167
+ continuation, but *not* doing this affects all clients every
1168
+ time someone pastes in console copy/paste from an affected
1169
+ platform. */
11651170
const xmsg = msg.split('\n');
11661171
xmsg.forEach(function(line,ndx){
1167
- xmsg[ndx] = line.trimRight();
1172
+ if(!f.markdownContinuation.test(line)){
1173
+ xmsg[ndx] = line.trimRight();
1174
+ }
11681175
});
11691176
msg = xmsg.join('\n');
11701177
}
11711178
if(msg) fd.set('msg',msg);
11721179
const file = BlobXferState.blob || this.e.inputFile.files[0];
@@ -1311,11 +1318,12 @@
13111318
elements stay in sync with the config UI via those settings
13121319
events.
13131320
*/
13141321
const settingsOps = [{
13151322
label: "Ctrl-enter to Send",
1316
- hint: "When on, only Ctrl-Enter will send messages. "+
1323
+ hint: "When on, only Ctrl-Enter will send messages and Enter adds "+
1324
+ "blank lines. "+
13171325
"When off, both Enter and Ctrl-Enter send. "+
13181326
"When the input field has focus, is empty, and preview "+
13191327
"mode is NOT active then Ctrl-Enter toggles this setting.",
13201328
boolValue: 'edit-ctrl-send'
13211329
},{
@@ -1376,12 +1384,11 @@
13761384
selectSound.selectedIndex = firstSoundIndex;
13771385
}
13781386
}
13791387
Chat.setNewMessageSound(selectSound.value);
13801388
settingsOps.push({
1381
- label: "Audio alert",
1382
- hint: "How to enable audio playback is browser-specific!",
1389
+ hint: "Audio alert. How to enable audio playback is browser-specific!",
13831390
select: selectSound,
13841391
callback: function(ev){
13851392
const v = ev.target.value;
13861393
Chat.setNewMessageSound(v);
13871394
F.toast.message("Audio notifications "+(v ? "enabled" : "disabled")+".");
@@ -1392,18 +1399,24 @@
13921399
/**
13931400
Build UI for config options...
13941401
*/
13951402
settingsOps.forEach(function f(op){
13961403
const line = D.addClass(D.div(), 'menu-entry');
1397
- const btn = D.append(
1404
+ const label = op.label ? D.append(
13981405
D.addClass(D.label(), 'cbutton'/*bootstrap skin hijacks 'button'*/),
1399
- op.label);
1406
+ op.label) : undefined;
1407
+ const labelWrapper = D.addClass(D.div(), 'label-wrapper');
1408
+ var hint;
1409
+ const col0 = D.span();
14001410
if(op.hint){
1401
- D.append(btn,D.br(),D.append(D.span(),op.hint));
1411
+ hint = D.append(D.addClass(D.span(),'hint'),op.hint);
14021412
}
14031413
if(op.hasOwnProperty('select')){
1404
- D.append(line, btn, op.select);
1414
+ D.append(line, col0, labelWrapper);
1415
+ D.append(labelWrapper, op.select);
1416
+ if(hint) D.append(labelWrapper, hint);
1417
+ if(label) D.append(col0, label);
14051418
if(op.callback){
14061419
op.select.addEventListener('change', (ev)=>op.callback(ev), false);
14071420
}
14081421
}else if(op.hasOwnProperty('boolValue')){
14091422
if(undefined === f.$id) f.$id = 0;
@@ -1418,16 +1431,22 @@
14181431
'aria-label', op.label);
14191432
const id = 'cfgopt'+f.$id;
14201433
check.checked = op.boolValue();
14211434
op.checkbox = check;
14221435
D.attr(check, 'id', id);
1423
- D.attr(btn, 'for', id);
1424
- D.append(line, check);
1425
- D.append(line, btn);
1436
+ D.append(line, col0, labelWrapper);
1437
+ D.append(col0, check);
1438
+ if(label){
1439
+ D.attr(label, 'for', id);
1440
+ D.append(labelWrapper, label);
1441
+ }
1442
+ if(hint) D.append(labelWrapper, hint);
14261443
}else{
14271444
line.addEventListener('click', callback);
1428
- D.append(line, btn);
1445
+ D.append(line, col0, labelWrapper);
1446
+ if(label) D.append(labelWrapper, label);
1447
+ if(hint) D.append(labelWrapper, hint);
14291448
}
14301449
D.append(optionsMenu, line);
14311450
if(op.persistentSetting){
14321451
Chat.settings.addListener(
14331452
op.persistentSetting,
14341453
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -1045,11 +1045,11 @@
1045 };
1046 /** Updates the paste/drop zone with details of the pasted/dropped
1047 data. The argument must be a Blob or Blob-like object (File) or
1048 it can be falsy to reset/clear that state.*/
1049 const updateDropZoneContent = function(blob){
1050 console.debug("updateDropZoneContent()",blob);
1051 const dd = bxs.dropDetails;
1052 bxs.blob = blob;
1053 D.clearElement(dd);
1054 if(!blob){
1055 Chat.e.inputFile.value = '';
@@ -1152,21 +1152,28 @@
1152 empty, this is a no-op.
1153 */
1154 Chat.submitMessage = function f(){
1155 if(!f.spaces){
1156 f.spaces = /\s+$/;
 
1157 }
1158 this.setCurrentView(this.e.viewMessages);
1159 const fd = new FormData();
1160 var msg = this.inputValue().trim();
1161 if(msg && (msg.indexOf('\n')>0 || f.spaces.test(msg))){
1162 /* Cosmetic: trim whitespace from the ends of lines to try to
1163 keep copy/paste from terminals, especially wide ones, from
1164 forcing a horizontal scrollbar on all clients. */
 
 
 
 
1165 const xmsg = msg.split('\n');
1166 xmsg.forEach(function(line,ndx){
1167 xmsg[ndx] = line.trimRight();
 
 
1168 });
1169 msg = xmsg.join('\n');
1170 }
1171 if(msg) fd.set('msg',msg);
1172 const file = BlobXferState.blob || this.e.inputFile.files[0];
@@ -1311,11 +1318,12 @@
1311 elements stay in sync with the config UI via those settings
1312 events.
1313 */
1314 const settingsOps = [{
1315 label: "Ctrl-enter to Send",
1316 hint: "When on, only Ctrl-Enter will send messages. "+
 
1317 "When off, both Enter and Ctrl-Enter send. "+
1318 "When the input field has focus, is empty, and preview "+
1319 "mode is NOT active then Ctrl-Enter toggles this setting.",
1320 boolValue: 'edit-ctrl-send'
1321 },{
@@ -1376,12 +1384,11 @@
1376 selectSound.selectedIndex = firstSoundIndex;
1377 }
1378 }
1379 Chat.setNewMessageSound(selectSound.value);
1380 settingsOps.push({
1381 label: "Audio alert",
1382 hint: "How to enable audio playback is browser-specific!",
1383 select: selectSound,
1384 callback: function(ev){
1385 const v = ev.target.value;
1386 Chat.setNewMessageSound(v);
1387 F.toast.message("Audio notifications "+(v ? "enabled" : "disabled")+".");
@@ -1392,18 +1399,24 @@
1392 /**
1393 Build UI for config options...
1394 */
1395 settingsOps.forEach(function f(op){
1396 const line = D.addClass(D.div(), 'menu-entry');
1397 const btn = D.append(
1398 D.addClass(D.label(), 'cbutton'/*bootstrap skin hijacks 'button'*/),
1399 op.label);
 
 
 
1400 if(op.hint){
1401 D.append(btn,D.br(),D.append(D.span(),op.hint));
1402 }
1403 if(op.hasOwnProperty('select')){
1404 D.append(line, btn, op.select);
 
 
 
1405 if(op.callback){
1406 op.select.addEventListener('change', (ev)=>op.callback(ev), false);
1407 }
1408 }else if(op.hasOwnProperty('boolValue')){
1409 if(undefined === f.$id) f.$id = 0;
@@ -1418,16 +1431,22 @@
1418 'aria-label', op.label);
1419 const id = 'cfgopt'+f.$id;
1420 check.checked = op.boolValue();
1421 op.checkbox = check;
1422 D.attr(check, 'id', id);
1423 D.attr(btn, 'for', id);
1424 D.append(line, check);
1425 D.append(line, btn);
 
 
 
 
1426 }else{
1427 line.addEventListener('click', callback);
1428 D.append(line, btn);
 
 
1429 }
1430 D.append(optionsMenu, line);
1431 if(op.persistentSetting){
1432 Chat.settings.addListener(
1433 op.persistentSetting,
1434
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -1045,11 +1045,11 @@
1045 };
1046 /** Updates the paste/drop zone with details of the pasted/dropped
1047 data. The argument must be a Blob or Blob-like object (File) or
1048 it can be falsy to reset/clear that state.*/
1049 const updateDropZoneContent = function(blob){
1050 //console.debug("updateDropZoneContent()",blob);
1051 const dd = bxs.dropDetails;
1052 bxs.blob = blob;
1053 D.clearElement(dd);
1054 if(!blob){
1055 Chat.e.inputFile.value = '';
@@ -1152,21 +1152,28 @@
1152 empty, this is a no-op.
1153 */
1154 Chat.submitMessage = function f(){
1155 if(!f.spaces){
1156 f.spaces = /\s+$/;
1157 f.markdownContinuation = /\\\s\s$/;
1158 }
1159 this.setCurrentView(this.e.viewMessages);
1160 const fd = new FormData();
1161 var msg = this.inputValue().trim();
1162 if(msg && (msg.indexOf('\n')>0 || f.spaces.test(msg))){
1163 /* Cosmetic: trim whitespace from the ends of lines to try to
1164 keep copy/paste from terminals, especially wide ones, from
1165 forcing a horizontal scrollbar on all clients. This breaks
1166 markdown's use of blackslash-space-space for paragraph
1167 continuation, but *not* doing this affects all clients every
1168 time someone pastes in console copy/paste from an affected
1169 platform. */
1170 const xmsg = msg.split('\n');
1171 xmsg.forEach(function(line,ndx){
1172 if(!f.markdownContinuation.test(line)){
1173 xmsg[ndx] = line.trimRight();
1174 }
1175 });
1176 msg = xmsg.join('\n');
1177 }
1178 if(msg) fd.set('msg',msg);
1179 const file = BlobXferState.blob || this.e.inputFile.files[0];
@@ -1311,11 +1318,12 @@
1318 elements stay in sync with the config UI via those settings
1319 events.
1320 */
1321 const settingsOps = [{
1322 label: "Ctrl-enter to Send",
1323 hint: "When on, only Ctrl-Enter will send messages and Enter adds "+
1324 "blank lines. "+
1325 "When off, both Enter and Ctrl-Enter send. "+
1326 "When the input field has focus, is empty, and preview "+
1327 "mode is NOT active then Ctrl-Enter toggles this setting.",
1328 boolValue: 'edit-ctrl-send'
1329 },{
@@ -1376,12 +1384,11 @@
1384 selectSound.selectedIndex = firstSoundIndex;
1385 }
1386 }
1387 Chat.setNewMessageSound(selectSound.value);
1388 settingsOps.push({
1389 hint: "Audio alert. How to enable audio playback is browser-specific!",
 
1390 select: selectSound,
1391 callback: function(ev){
1392 const v = ev.target.value;
1393 Chat.setNewMessageSound(v);
1394 F.toast.message("Audio notifications "+(v ? "enabled" : "disabled")+".");
@@ -1392,18 +1399,24 @@
1399 /**
1400 Build UI for config options...
1401 */
1402 settingsOps.forEach(function f(op){
1403 const line = D.addClass(D.div(), 'menu-entry');
1404 const label = op.label ? D.append(
1405 D.addClass(D.label(), 'cbutton'/*bootstrap skin hijacks 'button'*/),
1406 op.label) : undefined;
1407 const labelWrapper = D.addClass(D.div(), 'label-wrapper');
1408 var hint;
1409 const col0 = D.span();
1410 if(op.hint){
1411 hint = D.append(D.addClass(D.span(),'hint'),op.hint);
1412 }
1413 if(op.hasOwnProperty('select')){
1414 D.append(line, col0, labelWrapper);
1415 D.append(labelWrapper, op.select);
1416 if(hint) D.append(labelWrapper, hint);
1417 if(label) D.append(col0, label);
1418 if(op.callback){
1419 op.select.addEventListener('change', (ev)=>op.callback(ev), false);
1420 }
1421 }else if(op.hasOwnProperty('boolValue')){
1422 if(undefined === f.$id) f.$id = 0;
@@ -1418,16 +1431,22 @@
1431 'aria-label', op.label);
1432 const id = 'cfgopt'+f.$id;
1433 check.checked = op.boolValue();
1434 op.checkbox = check;
1435 D.attr(check, 'id', id);
1436 D.append(line, col0, labelWrapper);
1437 D.append(col0, check);
1438 if(label){
1439 D.attr(label, 'for', id);
1440 D.append(labelWrapper, label);
1441 }
1442 if(hint) D.append(labelWrapper, hint);
1443 }else{
1444 line.addEventListener('click', callback);
1445 D.append(line, col0, labelWrapper);
1446 if(label) D.append(labelWrapper, label);
1447 if(hint) D.append(labelWrapper, hint);
1448 }
1449 D.append(optionsMenu, line);
1450 if(op.persistentSetting){
1451 Chat.settings.addListener(
1452 op.persistentSetting,
1453
--- src/style.chat.css
+++ src/style.chat.css
@@ -336,23 +336,31 @@
336336
align-items: baseline;
337337
flex-direction: row;
338338
flex-wrap: nowrap;
339339
padding: 1em;
340340
}
341
-body.chat #chat-config #chat-config-options .menu-entry > label {
341
+body.chat #chat-config #chat-config-options .menu-entry label[for] {
342342
cursor: pointer;
343343
}
344
-body.chat #chat-config #chat-config-options .menu-entry > label > span {
344
+body.chat #chat-config #chat-config-options .menu-entry > *:first-child {
345
+ min-width: 1.5rem;
346
+}
347
+body.chat #chat-config #chat-config-options .menu-entry span.hint {
345348
/* Config menu hint text */
346349
font-size: 80%;
347350
white-space: pre-wrap;
351
+ display: inline-block;
352
+}
353
+body.chat #chat-config #chat-config-options .menu-entry:first-child {
348354
}
349
-body.chat #chat-config #chat-config-options .menu-entry > input:first-child {
350
- margin-right: 1em;
355
+body.chat #chat-config #chat-config-options .menu-entry div.label-wrapper {
356
+ display: flex;
357
+ flex-direction: column;
358
+ align-self: baseline;
359
+ margin-left: 1em;
351360
}
352
-body.chat #chat-config #chat-config-options .menu-entry > label:first-child {
353
- margin-right: 0.5em;
361
+body.chat #chat-config #chat-config-options .menu-entry select {
354362
}
355363
body.chat #chat-preview #chat-preview-content {
356364
overflow: auto;
357365
flex: 1 1 auto;
358366
padding: 0.5em;
359367
--- src/style.chat.css
+++ src/style.chat.css
@@ -336,23 +336,31 @@
336 align-items: baseline;
337 flex-direction: row;
338 flex-wrap: nowrap;
339 padding: 1em;
340 }
341 body.chat #chat-config #chat-config-options .menu-entry > label {
342 cursor: pointer;
343 }
344 body.chat #chat-config #chat-config-options .menu-entry > label > span {
 
 
 
345 /* Config menu hint text */
346 font-size: 80%;
347 white-space: pre-wrap;
 
 
 
348 }
349 body.chat #chat-config #chat-config-options .menu-entry > input:first-child {
350 margin-right: 1em;
 
 
 
351 }
352 body.chat #chat-config #chat-config-options .menu-entry > label:first-child {
353 margin-right: 0.5em;
354 }
355 body.chat #chat-preview #chat-preview-content {
356 overflow: auto;
357 flex: 1 1 auto;
358 padding: 0.5em;
359
--- src/style.chat.css
+++ src/style.chat.css
@@ -336,23 +336,31 @@
336 align-items: baseline;
337 flex-direction: row;
338 flex-wrap: nowrap;
339 padding: 1em;
340 }
341 body.chat #chat-config #chat-config-options .menu-entry label[for] {
342 cursor: pointer;
343 }
344 body.chat #chat-config #chat-config-options .menu-entry > *:first-child {
345 min-width: 1.5rem;
346 }
347 body.chat #chat-config #chat-config-options .menu-entry span.hint {
348 /* Config menu hint text */
349 font-size: 80%;
350 white-space: pre-wrap;
351 display: inline-block;
352 }
353 body.chat #chat-config #chat-config-options .menu-entry:first-child {
354 }
355 body.chat #chat-config #chat-config-options .menu-entry div.label-wrapper {
356 display: flex;
357 flex-direction: column;
358 align-self: baseline;
359 margin-left: 1em;
360 }
361 body.chat #chat-config #chat-config-options .menu-entry select {
 
362 }
363 body.chat #chat-preview #chat-preview-content {
364 overflow: auto;
365 flex: 1 1 auto;
366 padding: 0.5em;
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