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.
Commit
cbc7f117e6292f01ad1e5dbca6d78dfef6522d685c469d93fe2692f0fc3256c2
Parent
c4484625967fb1f…
2 files changed
+33
-14
+14
-6
+33
-14
| --- src/fossil.page.chat.js | ||
| +++ src/fossil.page.chat.js | ||
| @@ -1045,11 +1045,11 @@ | ||
| 1045 | 1045 | }; |
| 1046 | 1046 | /** Updates the paste/drop zone with details of the pasted/dropped |
| 1047 | 1047 | data. The argument must be a Blob or Blob-like object (File) or |
| 1048 | 1048 | it can be falsy to reset/clear that state.*/ |
| 1049 | 1049 | const updateDropZoneContent = function(blob){ |
| 1050 | - console.debug("updateDropZoneContent()",blob); | |
| 1050 | + //console.debug("updateDropZoneContent()",blob); | |
| 1051 | 1051 | const dd = bxs.dropDetails; |
| 1052 | 1052 | bxs.blob = blob; |
| 1053 | 1053 | D.clearElement(dd); |
| 1054 | 1054 | if(!blob){ |
| 1055 | 1055 | Chat.e.inputFile.value = ''; |
| @@ -1152,21 +1152,28 @@ | ||
| 1152 | 1152 | empty, this is a no-op. |
| 1153 | 1153 | */ |
| 1154 | 1154 | Chat.submitMessage = function f(){ |
| 1155 | 1155 | if(!f.spaces){ |
| 1156 | 1156 | f.spaces = /\s+$/; |
| 1157 | + f.markdownContinuation = /\\\s\s$/; | |
| 1157 | 1158 | } |
| 1158 | 1159 | this.setCurrentView(this.e.viewMessages); |
| 1159 | 1160 | const fd = new FormData(); |
| 1160 | 1161 | var msg = this.inputValue().trim(); |
| 1161 | 1162 | if(msg && (msg.indexOf('\n')>0 || f.spaces.test(msg))){ |
| 1162 | 1163 | /* Cosmetic: trim whitespace from the ends of lines to try to |
| 1163 | 1164 | 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. */ | |
| 1165 | 1170 | const xmsg = msg.split('\n'); |
| 1166 | 1171 | xmsg.forEach(function(line,ndx){ |
| 1167 | - xmsg[ndx] = line.trimRight(); | |
| 1172 | + if(!f.markdownContinuation.test(line)){ | |
| 1173 | + xmsg[ndx] = line.trimRight(); | |
| 1174 | + } | |
| 1168 | 1175 | }); |
| 1169 | 1176 | msg = xmsg.join('\n'); |
| 1170 | 1177 | } |
| 1171 | 1178 | if(msg) fd.set('msg',msg); |
| 1172 | 1179 | const file = BlobXferState.blob || this.e.inputFile.files[0]; |
| @@ -1311,11 +1318,12 @@ | ||
| 1311 | 1318 | elements stay in sync with the config UI via those settings |
| 1312 | 1319 | events. |
| 1313 | 1320 | */ |
| 1314 | 1321 | const settingsOps = [{ |
| 1315 | 1322 | 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. "+ | |
| 1317 | 1325 | "When off, both Enter and Ctrl-Enter send. "+ |
| 1318 | 1326 | "When the input field has focus, is empty, and preview "+ |
| 1319 | 1327 | "mode is NOT active then Ctrl-Enter toggles this setting.", |
| 1320 | 1328 | boolValue: 'edit-ctrl-send' |
| 1321 | 1329 | },{ |
| @@ -1376,12 +1384,11 @@ | ||
| 1376 | 1384 | selectSound.selectedIndex = firstSoundIndex; |
| 1377 | 1385 | } |
| 1378 | 1386 | } |
| 1379 | 1387 | Chat.setNewMessageSound(selectSound.value); |
| 1380 | 1388 | 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!", | |
| 1383 | 1390 | select: selectSound, |
| 1384 | 1391 | callback: function(ev){ |
| 1385 | 1392 | const v = ev.target.value; |
| 1386 | 1393 | Chat.setNewMessageSound(v); |
| 1387 | 1394 | F.toast.message("Audio notifications "+(v ? "enabled" : "disabled")+"."); |
| @@ -1392,18 +1399,24 @@ | ||
| 1392 | 1399 | /** |
| 1393 | 1400 | Build UI for config options... |
| 1394 | 1401 | */ |
| 1395 | 1402 | settingsOps.forEach(function f(op){ |
| 1396 | 1403 | const line = D.addClass(D.div(), 'menu-entry'); |
| 1397 | - const btn = D.append( | |
| 1404 | + const label = op.label ? D.append( | |
| 1398 | 1405 | 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(); | |
| 1400 | 1410 | 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); | |
| 1402 | 1412 | } |
| 1403 | 1413 | 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); | |
| 1405 | 1418 | if(op.callback){ |
| 1406 | 1419 | op.select.addEventListener('change', (ev)=>op.callback(ev), false); |
| 1407 | 1420 | } |
| 1408 | 1421 | }else if(op.hasOwnProperty('boolValue')){ |
| 1409 | 1422 | if(undefined === f.$id) f.$id = 0; |
| @@ -1418,16 +1431,22 @@ | ||
| 1418 | 1431 | 'aria-label', op.label); |
| 1419 | 1432 | const id = 'cfgopt'+f.$id; |
| 1420 | 1433 | check.checked = op.boolValue(); |
| 1421 | 1434 | op.checkbox = check; |
| 1422 | 1435 | 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); | |
| 1426 | 1443 | }else{ |
| 1427 | 1444 | 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); | |
| 1429 | 1448 | } |
| 1430 | 1449 | D.append(optionsMenu, line); |
| 1431 | 1450 | if(op.persistentSetting){ |
| 1432 | 1451 | Chat.settings.addListener( |
| 1433 | 1452 | op.persistentSetting, |
| 1434 | 1453 |
| --- 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 |
+14
-6
| --- src/style.chat.css | ||
| +++ src/style.chat.css | ||
| @@ -336,23 +336,31 @@ | ||
| 336 | 336 | align-items: baseline; |
| 337 | 337 | flex-direction: row; |
| 338 | 338 | flex-wrap: nowrap; |
| 339 | 339 | padding: 1em; |
| 340 | 340 | } |
| 341 | -body.chat #chat-config #chat-config-options .menu-entry > label { | |
| 341 | +body.chat #chat-config #chat-config-options .menu-entry label[for] { | |
| 342 | 342 | cursor: pointer; |
| 343 | 343 | } |
| 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 { | |
| 345 | 348 | /* Config menu hint text */ |
| 346 | 349 | font-size: 80%; |
| 347 | 350 | white-space: pre-wrap; |
| 351 | + display: inline-block; | |
| 352 | +} | |
| 353 | +body.chat #chat-config #chat-config-options .menu-entry:first-child { | |
| 348 | 354 | } |
| 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; | |
| 351 | 360 | } |
| 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 { | |
| 354 | 362 | } |
| 355 | 363 | body.chat #chat-preview #chat-preview-content { |
| 356 | 364 | overflow: auto; |
| 357 | 365 | flex: 1 1 auto; |
| 358 | 366 | padding: 0.5em; |
| 359 | 367 |
| --- 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 |