Fossil SCM

Reworked /chat input options for better cross-browser portability. Made /chat config friendlier for right-handed and mobile use.

stephan 2021-10-10 06:33 trunk merge
Commit db54f4b7066c564301f18af876dd12b66cc7fc0c28588115155ab13f3fb5263b
+11 -3
--- src/chat.c
+++ src/chat.c
@@ -174,15 +174,23 @@
174174
zInputPlaceholder0 =
175175
mprintf("Type markdown-formatted message for %h.", zProjectName);
176176
style_set_current_feature("chat");
177177
style_header("Chat");
178178
@ <div id='chat-input-area'>
179
- @ <div id='chat-input-line' class='single-line'>
180
- @ <div contenteditable id="chat-input-field" \
179
+ @ <div id='chat-input-line-wrapper' class='compact'>
180
+ @ <input type="text" id="chat-input-field-single" \
181
+ @ data-placeholder0="%h(zInputPlaceholder0)" \
182
+ @ data-placeholder="%h(zInputPlaceholder0)" \
183
+ @ class="chat-input-field"></input>
184
+ @ <textarea id="chat-input-field-multi" \
185
+ @ data-placeholder0="%h(zInputPlaceholder0)" \
186
+ @ data-placeholder="%h(zInputPlaceholder0)" \
187
+ @ class="chat-input-field hidden"></textarea>
188
+ @ <div contenteditable id="chat-input-field-x" \
181189
@ data-placeholder0="%h(zInputPlaceholder0)" \
182190
@ data-placeholder="%h(zInputPlaceholder0)" \
183
- @ class=""></div>
191
+ @ class="chat-input-field hidden"></div>
184192
@ <div id='chat-buttons-wrapper'>
185193
@ <span class='cbutton' id="chat-button-preview" \
186194
@ title="Preview message (Shift-Enter)">&#128065;</span>
187195
@ <span class='cbutton' id="chat-button-attach" \
188196
@ title="Attach file to message">%s(zPaperclip)</span>
189197
--- src/chat.c
+++ src/chat.c
@@ -174,15 +174,23 @@
174 zInputPlaceholder0 =
175 mprintf("Type markdown-formatted message for %h.", zProjectName);
176 style_set_current_feature("chat");
177 style_header("Chat");
178 @ <div id='chat-input-area'>
179 @ <div id='chat-input-line' class='single-line'>
180 @ <div contenteditable id="chat-input-field" \
 
 
 
 
 
 
 
 
181 @ data-placeholder0="%h(zInputPlaceholder0)" \
182 @ data-placeholder="%h(zInputPlaceholder0)" \
183 @ class=""></div>
184 @ <div id='chat-buttons-wrapper'>
185 @ <span class='cbutton' id="chat-button-preview" \
186 @ title="Preview message (Shift-Enter)">&#128065;</span>
187 @ <span class='cbutton' id="chat-button-attach" \
188 @ title="Attach file to message">%s(zPaperclip)</span>
189
--- src/chat.c
+++ src/chat.c
@@ -174,15 +174,23 @@
174 zInputPlaceholder0 =
175 mprintf("Type markdown-formatted message for %h.", zProjectName);
176 style_set_current_feature("chat");
177 style_header("Chat");
178 @ <div id='chat-input-area'>
179 @ <div id='chat-input-line-wrapper' class='compact'>
180 @ <input type="text" id="chat-input-field-single" \
181 @ data-placeholder0="%h(zInputPlaceholder0)" \
182 @ data-placeholder="%h(zInputPlaceholder0)" \
183 @ class="chat-input-field"></input>
184 @ <textarea id="chat-input-field-multi" \
185 @ data-placeholder0="%h(zInputPlaceholder0)" \
186 @ data-placeholder="%h(zInputPlaceholder0)" \
187 @ class="chat-input-field hidden"></textarea>
188 @ <div contenteditable id="chat-input-field-x" \
189 @ data-placeholder0="%h(zInputPlaceholder0)" \
190 @ data-placeholder="%h(zInputPlaceholder0)" \
191 @ class="chat-input-field hidden"></div>
192 @ <div id='chat-buttons-wrapper'>
193 @ <span class='cbutton' id="chat-button-preview" \
194 @ title="Preview message (Shift-Enter)">&#128065;</span>
195 @ <span class='cbutton' id="chat-button-attach" \
196 @ title="Attach file to message">%s(zPaperclip)</span>
197
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -94,11 +94,11 @@
9494
ht = wh;
9595
}else{
9696
elemsToCount.forEach((e)=>e ? extra += D.effectiveHeight(e) : false);
9797
ht = wh - extra;
9898
}
99
- f.chat.e.inputField.style.maxHeight = (ht/2)+"px";
99
+ f.chat.e.inputX.style.maxHeight = (ht/2)+"px";
100100
/* ^^^^ this is a middle ground between having no size cap
101101
on the input field and having a fixed arbitrary cap. */;
102102
contentArea.style.height =
103103
contentArea.style.maxHeight = [
104104
"calc(", (ht>=100 ? ht : 100), "px",
@@ -110,11 +110,11 @@
110110
if(false){
111111
console.debug("resized.",wh, extra, ht,
112112
window.getComputedStyle(contentArea).maxHeight,
113113
contentArea);
114114
console.debug("Set input max height to: ",
115
- f.chat.e.inputField.style.maxHeight);
115
+ f.chat.e.inputX.style.maxHeight);
116116
}
117117
};
118118
var doit;
119119
window.addEventListener('resize',function(ev){
120120
clearTimeout(doit);
@@ -131,16 +131,18 @@
131131
e:{/*map of certain DOM elements.*/
132132
messageInjectPoint: E1('#message-inject-point'),
133133
pageTitle: E1('head title'),
134134
loadOlderToolbar: undefined /* the load-posts toolbar (dynamically created) */,
135135
inputWrapper: E1("#chat-input-area"),
136
- inputLine: E1('#chat-input-line'),
136
+ inputElementWrapper: E1('#chat-input-line-wrapper'),
137137
fileSelectWrapper: E1('#chat-input-file-area'),
138138
viewMessages: E1('#chat-messages-wrapper'),
139139
btnSubmit: E1('#chat-button-submit'),
140140
btnAttach: E1('#chat-button-attach'),
141
- inputField: E1('#chat-input-field'),
141
+ inputX: E1('#chat-input-field-x'),
142
+ input1: E1('#chat-input-field-single'),
143
+ inputM: E1('#chat-input-field-multi'),
142144
inputFile: E1('#chat-input-file'),
143145
contentDiv: E1('div.content'),
144146
viewConfig: E1('#chat-config'),
145147
viewPreview: E1('#chat-preview'),
146148
previewContent: E1('#chat-preview-content'),
@@ -176,23 +178,24 @@
176178
taking into account single- vs multi-line input. The getter returns
177179
a string and the setter returns this object. */
178180
inputValue: function(){
179181
const e = this.inputElement();
180182
if(arguments.length){
181
- e.innerText = arguments[0];
183
+ if(e.isContentEditable) e.innerText = arguments[0];
184
+ else e.value = arguments[0];
182185
return this;
183186
}
184
- return e.innerText;
187
+ return e.isContentEditable ? e.innerText : e.value;
185188
},
186189
/** Asks the current user input field to take focus. Returns this. */
187190
inputFocus: function(){
188191
this.inputElement().focus();
189192
return this;
190193
},
191194
/** Returns the current message input element. */
192195
inputElement: function(){
193
- return this.e.inputField;
196
+ return this.e.inputFields[this.e.inputFields.$currentIndex];
194197
},
195198
/** Enables (if yes is truthy) or disables all elements in
196199
* this.disableDuringAjax. */
197200
enableAjaxComponents: function(yes){
198201
D[yes ? 'enable' : 'disable'](this.disableDuringAjax);
@@ -417,11 +420,17 @@
417420
timestamp of each user's most recent message. */
418421
"active-user-list-timestamps": false,
419422
/* When on, the [audible-alert] is played for one's own
420423
messages, else it is only played for other users'
421424
messages. */
422
- "alert-own-messages": false
425
+ "alert-own-messages": false,
426
+ /* "Experimental mode" input: use a contenteditable field
427
+ for input. This is generally more comfortable to use,
428
+ and more modern, than plain text input fields, but
429
+ the list of browser-specific quirks and bugs is...
430
+ not short. */
431
+ "edit-widget-x": false
423432
}
424433
},
425434
/** Plays a new-message notification sound IF the audible-alert
426435
setting is true, else this is a no-op. Returns this.
427436
*/
@@ -579,16 +588,22 @@
579588
D.addClassBriefly(e, a, 0, cb);
580589
}
581590
return this;
582591
}
583592
};
584
- if(D.attr(cs.e.inputField,'contenteditable','plaintext-only').isContentEditable){
593
+ cs.e.inputFields = [ cs.e.input1, cs.e.inputM, cs.e.inputX ];
594
+ cs.e.inputFields.$currentIndex = 0;
595
+ cs.e.inputFields.forEach(function(e,ndx){
596
+ if(ndx===cs.e.inputFields.$currentIndex) D.removeClass(e,'hidden');
597
+ else D.addClass(e,'hidden');
598
+ });
599
+ if(D.attr(cs.e.inputX,'contenteditable','plaintext-only').isContentEditable){
585600
cs.$browserHasPlaintextOnly = true;
586601
}else{
587602
/* Only the Chrome family supports contenteditable=plaintext-only */
588603
cs.$browserHasPlaintextOnly = false;
589
- D.attr(cs.e.inputField,'contenteditable','true');
604
+ D.attr(cs.e.inputX,'contenteditable','true');
590605
}
591606
cs.animate.$disabled = true;
592607
F.fetch.beforesend = ()=>cs.ajaxStart();
593608
F.fetch.aftersend = ()=>cs.ajaxEnd();
594609
cs.pageTitleOrig = cs.e.pageTitle.innerText;
@@ -1161,11 +1176,11 @@
11611176
/* Acrobatics to keep *some* installations of Firefox
11621177
from pasting formatting into contenteditable fields.
11631178
This also works on Chrome, but chrome has the
11641179
contenteditable=plaintext-only property which does this
11651180
for us. */
1166
- Chat.inputElement().addEventListener(
1181
+ Chat.e.inputX.addEventListener(
11671182
'paste',
11681183
function(ev){
11691184
if (ev.clipboardData && ev.clipboardData.getData) {
11701185
const pastedText = ev.clipboardData.getData('text/plain');
11711186
const selection = window.getSelection();
@@ -1185,15 +1200,12 @@
11851200
ev.dataTransfer.dropEffect = 'none';
11861201
ev.preventDefault();
11871202
ev.stopPropagation();
11881203
return false;
11891204
};
1190
-
11911205
['drop','dragenter','dragleave','dragend'].forEach(
1192
- (k)=>{
1193
- Chat.inputElement().addEventListener(k, noDragDropEvents, false);
1194
- }
1206
+ (k)=>Chat.e.inputX.addEventListener(k, noDragDropEvents, false)
11951207
);
11961208
return bxs;
11971209
})()/*drag/drop/paste*/;
11981210
11991211
const tzOffsetToString = function(off){
@@ -1323,11 +1335,13 @@
13231335
ev.stopPropagation();
13241336
Chat.submitMessage();
13251337
return false;
13261338
}
13271339
};
1328
- Chat.e.inputField.addEventListener('keydown', inputWidgetKeydown, false);
1340
+ Chat.e.inputFields.forEach(
1341
+ (e)=>e.addEventListener('keydown', inputWidgetKeydown, false)
1342
+ );
13291343
Chat.e.btnSubmit.addEventListener('click',(e)=>{
13301344
e.preventDefault();
13311345
Chat.submitMessage();
13321346
return false;
13331347
});
@@ -1383,70 +1397,98 @@
13831397
/**
13841398
Settings ops structure:
13851399
13861400
label: string for the UI
13871401
1388
- boolValue: string (name of Chat.settings setting) or a
1389
- function which returns true or false.
1402
+ boolValue: string (name of Chat.settings setting) or a function
1403
+ which returns true or false. If it is a string, it gets
1404
+ replaced by a function which returns
1405
+ Chat.settings.getBool(thatString) and the string gets assigned
1406
+ to the persistentSetting property of this object.
13901407
13911408
select: SELECT element (instead of boolValue)
13921409
13931410
callback: optional handler to call after setting is modified.
1411
+ Its "this" is the options object. If this object has a
1412
+ boolValue string or a persistentSetting property, the argument
1413
+ passed to the callback is a settings object in the form {key:K,
1414
+ value:V}. If this object does not have boolValue string or
1415
+ persistentSetting then the callback is passed an event object
1416
+ in response to the config option's UI widget being activated,
1417
+ normally a 'change' event.
1418
+
1419
+ children: [array of settings objects]. These get listed under
1420
+ this element and indented slightly for visual grouping. Only
1421
+ one level of indention is supported.
1422
+
1423
+ Elements which only have a label and maybe a hint and
1424
+ children can be used as headings.
13941425
1395
- If a setting has a boolValue set, that gets transformed into a
1426
+ If a setting has a boolValue set, that gets rendered as a
13961427
checkbox which toggles the given persistent setting (if
13971428
boolValue is a string) AND listens for changes to that setting
13981429
fired via Chat.settings.set() so that the checkbox can stay in
13991430
sync with external changes to that setting. Various Chat UI
14001431
elements stay in sync with the config UI via those settings
1401
- events.
1402
- */
1432
+ events. The checkbox element gets added to the options object
1433
+ so that the callback() can reference it via this.checkbox.
1434
+ */
14031435
const settingsOps = [{
1436
+ label: "Chat Configuration Options",
1437
+ hint: "Most of these settings are persistent via window.localStorage."
1438
+ },{
1439
+ label: "Chat-only mode",
1440
+ hint: "Toggle the page between normal fossil view and chat-only view.",
1441
+ boolValue: 'chat-only-mode'
1442
+ },{
14041443
label: "Ctrl-enter to Send",
1405
- hint: "When on, only Ctrl-Enter will send messages and Enter adds "+
1406
- "blank lines. "+
1407
- "When off, both Enter and Ctrl-Enter send. "+
1408
- "When the input field has focus, is empty, and preview "+
1409
- "mode is NOT active then Ctrl-Enter toggles this setting.",
1444
+ hint: [
1445
+ "When on, only Ctrl-Enter will send messages and Enter adds ",
1446
+ "blank lines. When off, both Enter and Ctrl-Enter send. ",
1447
+ "When the input field has focus, is empty, and preview ",
1448
+ "mode is NOT active then Ctrl-Enter toggles this setting."
1449
+ ].join(''),
14101450
boolValue: 'edit-ctrl-send'
14111451
},{
14121452
label: "Compact mode",
1413
- hint: "Toggle between a space-saving or more spacious writing area. "+
1414
- "When the input field has focus, is empty, and preview mode "+
1415
- "is NOT active then Shift-Enter toggles this setting.",
1453
+ hint: [
1454
+ "Toggle between a space-saving or more spacious writing area. ",
1455
+ "When the input field has focus, is empty, and preview mode ",
1456
+ "is NOT active then Shift-Enter toggles this setting."].join(''),
14161457
boolValue: 'edit-compact-mode'
14171458
},{
14181459
label: "Left-align my posts",
14191460
hint: "Default alignment of your own messages is selected "
1420
- +"based window width/height relationship.",
1461
+ + "based window width/height relationship.",
14211462
boolValue: ()=>!document.body.classList.contains('my-messages-right'),
14221463
callback: function f(){
14231464
document.body.classList[
14241465
this.checkbox.checked ? 'remove' : 'add'
14251466
]('my-messages-right');
14261467
}
14271468
},{
14281469
label: "Monospace message font",
1429
- hint: "Use monospace font for message text?",
1470
+ hint: "Use monospace font for message and input text.",
14301471
boolValue: 'monospace-messages',
14311472
callback: function(setting){
14321473
document.body.classList[
14331474
setting.value ? 'add' : 'remove'
14341475
]('monospace-messages');
14351476
}
14361477
},{
1437
- label: "Chat-only mode",
1438
- hint: "Toggle the page between normal fossil view and chat-only view.",
1439
- boolValue: 'chat-only-mode'
1478
+ label: "Show images inline",
1479
+ hint: "Show attached images inline or as a download link.",
1480
+ boolValue: 'images-inline'
14401481
},{
1441
- label: "Show images inline",
1442
- hint: "Whether to show images inline or as a hyperlink.",
1443
- boolValue: 'images-inline'
1444
- },namedOptions.activeUsers,{
1445
- label: "Timestamps in active users list",
1446
- hint: "Whether to show last-message timestamps.",
1447
- boolValue: 'active-user-list-timestamps'
1482
+ label: "Use 'contenteditable' editing mode.",
1483
+ boolValue: 'edit-widget-x',
1484
+ hint: [
1485
+ "When enabled, chat input uses a so-called 'contenteditable' ",
1486
+ "field. Though generally more comfortable and modern than ",
1487
+ "plain-text input fields, browser-specific quirks and bugs ",
1488
+ "may lead to frustration."
1489
+ ].join('')
14481490
}];
14491491
14501492
/** Set up selection list of notification sounds. */
14511493
if(1){
14521494
const selectSound = D.select();
@@ -1465,45 +1507,64 @@
14651507
selectSound.selectedIndex = firstSoundIndex;
14661508
}
14671509
}
14681510
Chat.setNewMessageSound(selectSound.value);
14691511
settingsOps.push({
1470
- hint: "Audio alert. How to enable audio playback is browser-specific!",
1471
- select: selectSound,
1472
- callback: function(ev){
1473
- const v = ev.target.value;
1474
- Chat.setNewMessageSound(v);
1475
- F.toast.message("Audio notifications "+(v ? "enabled" : "disabled")+".");
1476
- if(v) setTimeout(()=>Chat.playNewMessageSound(), 0);
1477
- }
1512
+ label: "Sound Options...",
1513
+ hint: "How to enable audio playback is browser-specific!",
1514
+ children:[{
1515
+ hint: "Audio alert",
1516
+ select: selectSound,
1517
+ callback: function(ev){
1518
+ const v = ev.target.value;
1519
+ Chat.setNewMessageSound(v);
1520
+ F.toast.message("Audio notifications "+(v ? "enabled" : "disabled")+".");
1521
+ if(v) setTimeout(()=>Chat.playNewMessageSound(), 0);
1522
+ }
1523
+ },{
1524
+ label: "Play notification for your own messages.",
1525
+ hint: "When enabled, the audio notification will be played for all messages, "+
1526
+ "including your own. When disabled only messages from other users "+
1527
+ "will trigger a notification.",
1528
+ boolValue: 'alert-own-messages'
1529
+ }]
14781530
});
14791531
}/*audio notification config*/
14801532
settingsOps.push({
1481
- label: "Play notification for your own messages.",
1482
- hint: "When enabled, the audio notification will be played for all messages, "+
1483
- "including your own. When disabled only messages from other users "+
1484
- "will trigger a notification.",
1485
- boolValue: 'alert-own-messages'
1533
+ label: "Active User List",
1534
+ hint: [
1535
+ "/chat cannot track active connections, but it can tell ",
1536
+ "you who has posted recently..."].join(''),
1537
+ children:[
1538
+ namedOptions.activeUsers,{
1539
+ label: "Timestamps in active users list",
1540
+ indent: true,
1541
+ hint: "Show most recent message timestamps in the active user list.",
1542
+ boolValue: 'active-user-list-timestamps'
1543
+ }
1544
+ ]
14861545
});
14871546
/**
14881547
Build UI for config options...
14891548
*/
1490
- settingsOps.forEach(function f(op){
1549
+ settingsOps.forEach(function f(op,indentOrIndex){
14911550
const line = D.addClass(D.div(), 'menu-entry');
1551
+ if(true===indentOrIndex) D.addClass(line, 'indent');
14921552
const label = op.label
14931553
? D.append(D.label(),op.label) : undefined;
14941554
const labelWrapper = D.addClass(D.div(), 'label-wrapper');
14951555
var hint;
1496
- const col0 = D.span();
14971556
if(op.hint){
14981557
hint = D.append(D.addClass(D.span(),'hint'),op.hint);
14991558
}
15001559
if(op.hasOwnProperty('select')){
1501
- D.append(line, col0, labelWrapper);
1560
+ const col0 = D.addClass(D.span(/*empty, but for spacing*/),
1561
+ 'toggle-wrapper');
1562
+ D.append(line, labelWrapper, col0);
15021563
D.append(labelWrapper, op.select);
15031564
if(hint) D.append(labelWrapper, hint);
1504
- if(label) D.append(col0, label);
1565
+ if(label) D.append(label);
15051566
if(op.callback){
15061567
op.select.addEventListener('change', (ev)=>op.callback(ev), false);
15071568
}
15081569
}else if(op.hasOwnProperty('boolValue')){
15091570
if(undefined === f.$id) f.$id = 0;
@@ -1515,23 +1576,26 @@
15151576
}
15161577
const check = op.checkbox
15171578
= D.attr(D.checkbox(1, op.boolValue()),
15181579
'aria-label', op.label);
15191580
const id = 'cfgopt'+f.$id;
1581
+ const col0 = D.addClass(D.span(), 'toggle-wrapper');
15201582
check.checked = op.boolValue();
15211583
op.checkbox = check;
15221584
D.attr(check, 'id', id);
1523
- D.append(line, col0, labelWrapper);
1585
+ D.append(line, labelWrapper, col0);
15241586
D.append(col0, check);
15251587
if(label){
15261588
D.attr(label, 'for', id);
15271589
D.append(labelWrapper, label);
15281590
}
15291591
if(hint) D.append(labelWrapper, hint);
15301592
}else{
1531
- line.addEventListener('click', callback);
1532
- D.append(line, col0, labelWrapper);
1593
+ if(op.callback){
1594
+ line.addEventListener('click', (ev)=>op.callback(ev));
1595
+ }
1596
+ D.append(line, labelWrapper);
15331597
if(label) D.append(labelWrapper, label);
15341598
if(hint) D.append(labelWrapper, hint);
15351599
}
15361600
D.append(optionsMenu, line);
15371601
if(op.persistentSetting){
@@ -1550,10 +1614,11 @@
15501614
}, false);
15511615
}
15521616
}else if(op.callback && op.checkbox){
15531617
op.checkbox.addEventListener('change', (ev)=>op.callback(ev), false);
15541618
}
1619
+ if(op.children) op.children.forEach((x)=>f(x,true));
15551620
});
15561621
})()/*#chat-button-settings setup*/;
15571622
15581623
(function(){
15591624
/* Install default settings... must come after
@@ -1569,20 +1634,58 @@
15691634
Chat.settings.addListener('active-user-list-timestamps',function(s){
15701635
Chat.showActiveUserTimestamps(s.value);
15711636
});
15721637
Chat.settings.addListener('chat-only-mode',function(s){
15731638
Chat.chatOnlyMode(s.value);
1639
+ });
1640
+ Chat.settings.addListener('edit-widget-x',function(s){
1641
+ let eSelected;
1642
+ if(s.value){
1643
+ if(Chat.e.inputX===Chat.inputElement()) return;
1644
+ eSelected = Chat.e.inputX;
1645
+ }else{
1646
+ eSelected = Chat.settings.getBool('edit-compact-mode')
1647
+ ? Chat.e.input1 : Chat.e.inputM;
1648
+ }
1649
+ const v = Chat.inputValue();
1650
+ Chat.inputValue('');
1651
+ Chat.e.inputFields.forEach(function(e,ndx){
1652
+ if(eSelected===e){
1653
+ Chat.e.inputFields.$currentIndex = ndx;
1654
+ D.removeClass(e, 'hidden');
1655
+ }
1656
+ else D.addClass(e,'hidden');
1657
+ });
1658
+ Chat.inputValue(v);
1659
+ eSelected.focus();
15741660
});
15751661
Chat.settings.addListener('edit-compact-mode',function(s){
1576
- Chat.e.inputLine.classList[
1662
+ if(Chat.e.inputX!==Chat.inputElement()){
1663
+ /* Text field/textarea mode: swap them if needed.
1664
+ Compact mode of inputX is toggled via CSS. */
1665
+ const a = s.value
1666
+ ? [Chat.e.input1, Chat.e.inputM, 0]
1667
+ : [Chat.e.inputM, Chat.e.input1, 1];
1668
+ const v = Chat.inputValue();
1669
+ Chat.inputValue('');
1670
+ Chat.e.inputFields.$currentIndex = a[2];
1671
+ Chat.inputValue(v);
1672
+ D.removeClass(a[0], 'hidden');
1673
+ D.addClass(a[1], 'hidden');
1674
+ }
1675
+ Chat.e.inputElementWrapper.classList[
15771676
s.value ? 'add' : 'remove'
15781677
]('compact');
1678
+ Chat.e.inputFields[Chat.e.inputFields.$currentIndex].focus();
15791679
});
15801680
Chat.settings.addListener('edit-ctrl-send',function(s){
15811681
const label = (s.value ? "Ctrl-" : "")+"Enter submits messages.";
1582
- const eInput = Chat.inputElement();
1583
- eInput.dataset.placeholder = eInput.dataset.placeholder0 + " " +label;
1682
+ Chat.e.inputFields.forEach((e)=>{
1683
+ const v = e.dataset.placeholder0 + " " +label;
1684
+ if(e.isContentEditable) e.dataset.placeholder = v;
1685
+ else D.attr(e,'placeholder',v);
1686
+ });
15841687
Chat.e.btnSubmit.title = label;
15851688
});
15861689
const valueKludges = {
15871690
/* Convert certain string-format values to other types... */
15881691
"false": false,
@@ -1607,11 +1710,11 @@
16071710
this.inputFocus();
16081711
};
16091712
Chat.e.viewPreview.querySelector('#chat-preview-close').
16101713
addEventListener('click', ()=>Chat.setCurrentView(Chat.e.viewMessages), false);
16111714
let previewPending = false;
1612
- const elemsToEnable = [btnPreview, Chat.e.btnSubmit, Chat.e.inputField];
1715
+ const elemsToEnable = [btnPreview, Chat.e.btnSubmit, Chat.e.inputFields];
16131716
const submit = function(ev){
16141717
ev.preventDefault();
16151718
ev.stopPropagation();
16161719
if(previewPending) return false;
16171720
const txt = Chat.inputValue();
16181721
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -94,11 +94,11 @@
94 ht = wh;
95 }else{
96 elemsToCount.forEach((e)=>e ? extra += D.effectiveHeight(e) : false);
97 ht = wh - extra;
98 }
99 f.chat.e.inputField.style.maxHeight = (ht/2)+"px";
100 /* ^^^^ this is a middle ground between having no size cap
101 on the input field and having a fixed arbitrary cap. */;
102 contentArea.style.height =
103 contentArea.style.maxHeight = [
104 "calc(", (ht>=100 ? ht : 100), "px",
@@ -110,11 +110,11 @@
110 if(false){
111 console.debug("resized.",wh, extra, ht,
112 window.getComputedStyle(contentArea).maxHeight,
113 contentArea);
114 console.debug("Set input max height to: ",
115 f.chat.e.inputField.style.maxHeight);
116 }
117 };
118 var doit;
119 window.addEventListener('resize',function(ev){
120 clearTimeout(doit);
@@ -131,16 +131,18 @@
131 e:{/*map of certain DOM elements.*/
132 messageInjectPoint: E1('#message-inject-point'),
133 pageTitle: E1('head title'),
134 loadOlderToolbar: undefined /* the load-posts toolbar (dynamically created) */,
135 inputWrapper: E1("#chat-input-area"),
136 inputLine: E1('#chat-input-line'),
137 fileSelectWrapper: E1('#chat-input-file-area'),
138 viewMessages: E1('#chat-messages-wrapper'),
139 btnSubmit: E1('#chat-button-submit'),
140 btnAttach: E1('#chat-button-attach'),
141 inputField: E1('#chat-input-field'),
 
 
142 inputFile: E1('#chat-input-file'),
143 contentDiv: E1('div.content'),
144 viewConfig: E1('#chat-config'),
145 viewPreview: E1('#chat-preview'),
146 previewContent: E1('#chat-preview-content'),
@@ -176,23 +178,24 @@
176 taking into account single- vs multi-line input. The getter returns
177 a string and the setter returns this object. */
178 inputValue: function(){
179 const e = this.inputElement();
180 if(arguments.length){
181 e.innerText = arguments[0];
 
182 return this;
183 }
184 return e.innerText;
185 },
186 /** Asks the current user input field to take focus. Returns this. */
187 inputFocus: function(){
188 this.inputElement().focus();
189 return this;
190 },
191 /** Returns the current message input element. */
192 inputElement: function(){
193 return this.e.inputField;
194 },
195 /** Enables (if yes is truthy) or disables all elements in
196 * this.disableDuringAjax. */
197 enableAjaxComponents: function(yes){
198 D[yes ? 'enable' : 'disable'](this.disableDuringAjax);
@@ -417,11 +420,17 @@
417 timestamp of each user's most recent message. */
418 "active-user-list-timestamps": false,
419 /* When on, the [audible-alert] is played for one's own
420 messages, else it is only played for other users'
421 messages. */
422 "alert-own-messages": false
 
 
 
 
 
 
423 }
424 },
425 /** Plays a new-message notification sound IF the audible-alert
426 setting is true, else this is a no-op. Returns this.
427 */
@@ -579,16 +588,22 @@
579 D.addClassBriefly(e, a, 0, cb);
580 }
581 return this;
582 }
583 };
584 if(D.attr(cs.e.inputField,'contenteditable','plaintext-only').isContentEditable){
 
 
 
 
 
 
585 cs.$browserHasPlaintextOnly = true;
586 }else{
587 /* Only the Chrome family supports contenteditable=plaintext-only */
588 cs.$browserHasPlaintextOnly = false;
589 D.attr(cs.e.inputField,'contenteditable','true');
590 }
591 cs.animate.$disabled = true;
592 F.fetch.beforesend = ()=>cs.ajaxStart();
593 F.fetch.aftersend = ()=>cs.ajaxEnd();
594 cs.pageTitleOrig = cs.e.pageTitle.innerText;
@@ -1161,11 +1176,11 @@
1161 /* Acrobatics to keep *some* installations of Firefox
1162 from pasting formatting into contenteditable fields.
1163 This also works on Chrome, but chrome has the
1164 contenteditable=plaintext-only property which does this
1165 for us. */
1166 Chat.inputElement().addEventListener(
1167 'paste',
1168 function(ev){
1169 if (ev.clipboardData && ev.clipboardData.getData) {
1170 const pastedText = ev.clipboardData.getData('text/plain');
1171 const selection = window.getSelection();
@@ -1185,15 +1200,12 @@
1185 ev.dataTransfer.dropEffect = 'none';
1186 ev.preventDefault();
1187 ev.stopPropagation();
1188 return false;
1189 };
1190
1191 ['drop','dragenter','dragleave','dragend'].forEach(
1192 (k)=>{
1193 Chat.inputElement().addEventListener(k, noDragDropEvents, false);
1194 }
1195 );
1196 return bxs;
1197 })()/*drag/drop/paste*/;
1198
1199 const tzOffsetToString = function(off){
@@ -1323,11 +1335,13 @@
1323 ev.stopPropagation();
1324 Chat.submitMessage();
1325 return false;
1326 }
1327 };
1328 Chat.e.inputField.addEventListener('keydown', inputWidgetKeydown, false);
 
 
1329 Chat.e.btnSubmit.addEventListener('click',(e)=>{
1330 e.preventDefault();
1331 Chat.submitMessage();
1332 return false;
1333 });
@@ -1383,70 +1397,98 @@
1383 /**
1384 Settings ops structure:
1385
1386 label: string for the UI
1387
1388 boolValue: string (name of Chat.settings setting) or a
1389 function which returns true or false.
 
 
 
1390
1391 select: SELECT element (instead of boolValue)
1392
1393 callback: optional handler to call after setting is modified.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1394
1395 If a setting has a boolValue set, that gets transformed into a
1396 checkbox which toggles the given persistent setting (if
1397 boolValue is a string) AND listens for changes to that setting
1398 fired via Chat.settings.set() so that the checkbox can stay in
1399 sync with external changes to that setting. Various Chat UI
1400 elements stay in sync with the config UI via those settings
1401 events.
1402 */
 
1403 const settingsOps = [{
 
 
 
 
 
 
 
1404 label: "Ctrl-enter to Send",
1405 hint: "When on, only Ctrl-Enter will send messages and Enter adds "+
1406 "blank lines. "+
1407 "When off, both Enter and Ctrl-Enter send. "+
1408 "When the input field has focus, is empty, and preview "+
1409 "mode is NOT active then Ctrl-Enter toggles this setting.",
 
1410 boolValue: 'edit-ctrl-send'
1411 },{
1412 label: "Compact mode",
1413 hint: "Toggle between a space-saving or more spacious writing area. "+
1414 "When the input field has focus, is empty, and preview mode "+
1415 "is NOT active then Shift-Enter toggles this setting.",
 
1416 boolValue: 'edit-compact-mode'
1417 },{
1418 label: "Left-align my posts",
1419 hint: "Default alignment of your own messages is selected "
1420 +"based window width/height relationship.",
1421 boolValue: ()=>!document.body.classList.contains('my-messages-right'),
1422 callback: function f(){
1423 document.body.classList[
1424 this.checkbox.checked ? 'remove' : 'add'
1425 ]('my-messages-right');
1426 }
1427 },{
1428 label: "Monospace message font",
1429 hint: "Use monospace font for message text?",
1430 boolValue: 'monospace-messages',
1431 callback: function(setting){
1432 document.body.classList[
1433 setting.value ? 'add' : 'remove'
1434 ]('monospace-messages');
1435 }
1436 },{
1437 label: "Chat-only mode",
1438 hint: "Toggle the page between normal fossil view and chat-only view.",
1439 boolValue: 'chat-only-mode'
1440 },{
1441 label: "Show images inline",
1442 hint: "Whether to show images inline or as a hyperlink.",
1443 boolValue: 'images-inline'
1444 },namedOptions.activeUsers,{
1445 label: "Timestamps in active users list",
1446 hint: "Whether to show last-message timestamps.",
1447 boolValue: 'active-user-list-timestamps'
 
1448 }];
1449
1450 /** Set up selection list of notification sounds. */
1451 if(1){
1452 const selectSound = D.select();
@@ -1465,45 +1507,64 @@
1465 selectSound.selectedIndex = firstSoundIndex;
1466 }
1467 }
1468 Chat.setNewMessageSound(selectSound.value);
1469 settingsOps.push({
1470 hint: "Audio alert. How to enable audio playback is browser-specific!",
1471 select: selectSound,
1472 callback: function(ev){
1473 const v = ev.target.value;
1474 Chat.setNewMessageSound(v);
1475 F.toast.message("Audio notifications "+(v ? "enabled" : "disabled")+".");
1476 if(v) setTimeout(()=>Chat.playNewMessageSound(), 0);
1477 }
 
 
 
 
 
 
 
 
 
 
1478 });
1479 }/*audio notification config*/
1480 settingsOps.push({
1481 label: "Play notification for your own messages.",
1482 hint: "When enabled, the audio notification will be played for all messages, "+
1483 "including your own. When disabled only messages from other users "+
1484 "will trigger a notification.",
1485 boolValue: 'alert-own-messages'
 
 
 
 
 
 
 
1486 });
1487 /**
1488 Build UI for config options...
1489 */
1490 settingsOps.forEach(function f(op){
1491 const line = D.addClass(D.div(), 'menu-entry');
 
1492 const label = op.label
1493 ? D.append(D.label(),op.label) : undefined;
1494 const labelWrapper = D.addClass(D.div(), 'label-wrapper');
1495 var hint;
1496 const col0 = D.span();
1497 if(op.hint){
1498 hint = D.append(D.addClass(D.span(),'hint'),op.hint);
1499 }
1500 if(op.hasOwnProperty('select')){
1501 D.append(line, col0, labelWrapper);
 
 
1502 D.append(labelWrapper, op.select);
1503 if(hint) D.append(labelWrapper, hint);
1504 if(label) D.append(col0, label);
1505 if(op.callback){
1506 op.select.addEventListener('change', (ev)=>op.callback(ev), false);
1507 }
1508 }else if(op.hasOwnProperty('boolValue')){
1509 if(undefined === f.$id) f.$id = 0;
@@ -1515,23 +1576,26 @@
1515 }
1516 const check = op.checkbox
1517 = D.attr(D.checkbox(1, op.boolValue()),
1518 'aria-label', op.label);
1519 const id = 'cfgopt'+f.$id;
 
1520 check.checked = op.boolValue();
1521 op.checkbox = check;
1522 D.attr(check, 'id', id);
1523 D.append(line, col0, labelWrapper);
1524 D.append(col0, check);
1525 if(label){
1526 D.attr(label, 'for', id);
1527 D.append(labelWrapper, label);
1528 }
1529 if(hint) D.append(labelWrapper, hint);
1530 }else{
1531 line.addEventListener('click', callback);
1532 D.append(line, col0, labelWrapper);
 
 
1533 if(label) D.append(labelWrapper, label);
1534 if(hint) D.append(labelWrapper, hint);
1535 }
1536 D.append(optionsMenu, line);
1537 if(op.persistentSetting){
@@ -1550,10 +1614,11 @@
1550 }, false);
1551 }
1552 }else if(op.callback && op.checkbox){
1553 op.checkbox.addEventListener('change', (ev)=>op.callback(ev), false);
1554 }
 
1555 });
1556 })()/*#chat-button-settings setup*/;
1557
1558 (function(){
1559 /* Install default settings... must come after
@@ -1569,20 +1634,58 @@
1569 Chat.settings.addListener('active-user-list-timestamps',function(s){
1570 Chat.showActiveUserTimestamps(s.value);
1571 });
1572 Chat.settings.addListener('chat-only-mode',function(s){
1573 Chat.chatOnlyMode(s.value);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1574 });
1575 Chat.settings.addListener('edit-compact-mode',function(s){
1576 Chat.e.inputLine.classList[
 
 
 
 
 
 
 
 
 
 
 
 
 
1577 s.value ? 'add' : 'remove'
1578 ]('compact');
 
1579 });
1580 Chat.settings.addListener('edit-ctrl-send',function(s){
1581 const label = (s.value ? "Ctrl-" : "")+"Enter submits messages.";
1582 const eInput = Chat.inputElement();
1583 eInput.dataset.placeholder = eInput.dataset.placeholder0 + " " +label;
 
 
 
1584 Chat.e.btnSubmit.title = label;
1585 });
1586 const valueKludges = {
1587 /* Convert certain string-format values to other types... */
1588 "false": false,
@@ -1607,11 +1710,11 @@
1607 this.inputFocus();
1608 };
1609 Chat.e.viewPreview.querySelector('#chat-preview-close').
1610 addEventListener('click', ()=>Chat.setCurrentView(Chat.e.viewMessages), false);
1611 let previewPending = false;
1612 const elemsToEnable = [btnPreview, Chat.e.btnSubmit, Chat.e.inputField];
1613 const submit = function(ev){
1614 ev.preventDefault();
1615 ev.stopPropagation();
1616 if(previewPending) return false;
1617 const txt = Chat.inputValue();
1618
--- src/fossil.page.chat.js
+++ src/fossil.page.chat.js
@@ -94,11 +94,11 @@
94 ht = wh;
95 }else{
96 elemsToCount.forEach((e)=>e ? extra += D.effectiveHeight(e) : false);
97 ht = wh - extra;
98 }
99 f.chat.e.inputX.style.maxHeight = (ht/2)+"px";
100 /* ^^^^ this is a middle ground between having no size cap
101 on the input field and having a fixed arbitrary cap. */;
102 contentArea.style.height =
103 contentArea.style.maxHeight = [
104 "calc(", (ht>=100 ? ht : 100), "px",
@@ -110,11 +110,11 @@
110 if(false){
111 console.debug("resized.",wh, extra, ht,
112 window.getComputedStyle(contentArea).maxHeight,
113 contentArea);
114 console.debug("Set input max height to: ",
115 f.chat.e.inputX.style.maxHeight);
116 }
117 };
118 var doit;
119 window.addEventListener('resize',function(ev){
120 clearTimeout(doit);
@@ -131,16 +131,18 @@
131 e:{/*map of certain DOM elements.*/
132 messageInjectPoint: E1('#message-inject-point'),
133 pageTitle: E1('head title'),
134 loadOlderToolbar: undefined /* the load-posts toolbar (dynamically created) */,
135 inputWrapper: E1("#chat-input-area"),
136 inputElementWrapper: E1('#chat-input-line-wrapper'),
137 fileSelectWrapper: E1('#chat-input-file-area'),
138 viewMessages: E1('#chat-messages-wrapper'),
139 btnSubmit: E1('#chat-button-submit'),
140 btnAttach: E1('#chat-button-attach'),
141 inputX: E1('#chat-input-field-x'),
142 input1: E1('#chat-input-field-single'),
143 inputM: E1('#chat-input-field-multi'),
144 inputFile: E1('#chat-input-file'),
145 contentDiv: E1('div.content'),
146 viewConfig: E1('#chat-config'),
147 viewPreview: E1('#chat-preview'),
148 previewContent: E1('#chat-preview-content'),
@@ -176,23 +178,24 @@
178 taking into account single- vs multi-line input. The getter returns
179 a string and the setter returns this object. */
180 inputValue: function(){
181 const e = this.inputElement();
182 if(arguments.length){
183 if(e.isContentEditable) e.innerText = arguments[0];
184 else e.value = arguments[0];
185 return this;
186 }
187 return e.isContentEditable ? e.innerText : e.value;
188 },
189 /** Asks the current user input field to take focus. Returns this. */
190 inputFocus: function(){
191 this.inputElement().focus();
192 return this;
193 },
194 /** Returns the current message input element. */
195 inputElement: function(){
196 return this.e.inputFields[this.e.inputFields.$currentIndex];
197 },
198 /** Enables (if yes is truthy) or disables all elements in
199 * this.disableDuringAjax. */
200 enableAjaxComponents: function(yes){
201 D[yes ? 'enable' : 'disable'](this.disableDuringAjax);
@@ -417,11 +420,17 @@
420 timestamp of each user's most recent message. */
421 "active-user-list-timestamps": false,
422 /* When on, the [audible-alert] is played for one's own
423 messages, else it is only played for other users'
424 messages. */
425 "alert-own-messages": false,
426 /* "Experimental mode" input: use a contenteditable field
427 for input. This is generally more comfortable to use,
428 and more modern, than plain text input fields, but
429 the list of browser-specific quirks and bugs is...
430 not short. */
431 "edit-widget-x": false
432 }
433 },
434 /** Plays a new-message notification sound IF the audible-alert
435 setting is true, else this is a no-op. Returns this.
436 */
@@ -579,16 +588,22 @@
588 D.addClassBriefly(e, a, 0, cb);
589 }
590 return this;
591 }
592 };
593 cs.e.inputFields = [ cs.e.input1, cs.e.inputM, cs.e.inputX ];
594 cs.e.inputFields.$currentIndex = 0;
595 cs.e.inputFields.forEach(function(e,ndx){
596 if(ndx===cs.e.inputFields.$currentIndex) D.removeClass(e,'hidden');
597 else D.addClass(e,'hidden');
598 });
599 if(D.attr(cs.e.inputX,'contenteditable','plaintext-only').isContentEditable){
600 cs.$browserHasPlaintextOnly = true;
601 }else{
602 /* Only the Chrome family supports contenteditable=plaintext-only */
603 cs.$browserHasPlaintextOnly = false;
604 D.attr(cs.e.inputX,'contenteditable','true');
605 }
606 cs.animate.$disabled = true;
607 F.fetch.beforesend = ()=>cs.ajaxStart();
608 F.fetch.aftersend = ()=>cs.ajaxEnd();
609 cs.pageTitleOrig = cs.e.pageTitle.innerText;
@@ -1161,11 +1176,11 @@
1176 /* Acrobatics to keep *some* installations of Firefox
1177 from pasting formatting into contenteditable fields.
1178 This also works on Chrome, but chrome has the
1179 contenteditable=plaintext-only property which does this
1180 for us. */
1181 Chat.e.inputX.addEventListener(
1182 'paste',
1183 function(ev){
1184 if (ev.clipboardData && ev.clipboardData.getData) {
1185 const pastedText = ev.clipboardData.getData('text/plain');
1186 const selection = window.getSelection();
@@ -1185,15 +1200,12 @@
1200 ev.dataTransfer.dropEffect = 'none';
1201 ev.preventDefault();
1202 ev.stopPropagation();
1203 return false;
1204 };
 
1205 ['drop','dragenter','dragleave','dragend'].forEach(
1206 (k)=>Chat.e.inputX.addEventListener(k, noDragDropEvents, false)
 
 
1207 );
1208 return bxs;
1209 })()/*drag/drop/paste*/;
1210
1211 const tzOffsetToString = function(off){
@@ -1323,11 +1335,13 @@
1335 ev.stopPropagation();
1336 Chat.submitMessage();
1337 return false;
1338 }
1339 };
1340 Chat.e.inputFields.forEach(
1341 (e)=>e.addEventListener('keydown', inputWidgetKeydown, false)
1342 );
1343 Chat.e.btnSubmit.addEventListener('click',(e)=>{
1344 e.preventDefault();
1345 Chat.submitMessage();
1346 return false;
1347 });
@@ -1383,70 +1397,98 @@
1397 /**
1398 Settings ops structure:
1399
1400 label: string for the UI
1401
1402 boolValue: string (name of Chat.settings setting) or a function
1403 which returns true or false. If it is a string, it gets
1404 replaced by a function which returns
1405 Chat.settings.getBool(thatString) and the string gets assigned
1406 to the persistentSetting property of this object.
1407
1408 select: SELECT element (instead of boolValue)
1409
1410 callback: optional handler to call after setting is modified.
1411 Its "this" is the options object. If this object has a
1412 boolValue string or a persistentSetting property, the argument
1413 passed to the callback is a settings object in the form {key:K,
1414 value:V}. If this object does not have boolValue string or
1415 persistentSetting then the callback is passed an event object
1416 in response to the config option's UI widget being activated,
1417 normally a 'change' event.
1418
1419 children: [array of settings objects]. These get listed under
1420 this element and indented slightly for visual grouping. Only
1421 one level of indention is supported.
1422
1423 Elements which only have a label and maybe a hint and
1424 children can be used as headings.
1425
1426 If a setting has a boolValue set, that gets rendered as a
1427 checkbox which toggles the given persistent setting (if
1428 boolValue is a string) AND listens for changes to that setting
1429 fired via Chat.settings.set() so that the checkbox can stay in
1430 sync with external changes to that setting. Various Chat UI
1431 elements stay in sync with the config UI via those settings
1432 events. The checkbox element gets added to the options object
1433 so that the callback() can reference it via this.checkbox.
1434 */
1435 const settingsOps = [{
1436 label: "Chat Configuration Options",
1437 hint: "Most of these settings are persistent via window.localStorage."
1438 },{
1439 label: "Chat-only mode",
1440 hint: "Toggle the page between normal fossil view and chat-only view.",
1441 boolValue: 'chat-only-mode'
1442 },{
1443 label: "Ctrl-enter to Send",
1444 hint: [
1445 "When on, only Ctrl-Enter will send messages and Enter adds ",
1446 "blank lines. When off, both Enter and Ctrl-Enter send. ",
1447 "When the input field has focus, is empty, and preview ",
1448 "mode is NOT active then Ctrl-Enter toggles this setting."
1449 ].join(''),
1450 boolValue: 'edit-ctrl-send'
1451 },{
1452 label: "Compact mode",
1453 hint: [
1454 "Toggle between a space-saving or more spacious writing area. ",
1455 "When the input field has focus, is empty, and preview mode ",
1456 "is NOT active then Shift-Enter toggles this setting."].join(''),
1457 boolValue: 'edit-compact-mode'
1458 },{
1459 label: "Left-align my posts",
1460 hint: "Default alignment of your own messages is selected "
1461 + "based window width/height relationship.",
1462 boolValue: ()=>!document.body.classList.contains('my-messages-right'),
1463 callback: function f(){
1464 document.body.classList[
1465 this.checkbox.checked ? 'remove' : 'add'
1466 ]('my-messages-right');
1467 }
1468 },{
1469 label: "Monospace message font",
1470 hint: "Use monospace font for message and input text.",
1471 boolValue: 'monospace-messages',
1472 callback: function(setting){
1473 document.body.classList[
1474 setting.value ? 'add' : 'remove'
1475 ]('monospace-messages');
1476 }
1477 },{
1478 label: "Show images inline",
1479 hint: "Show attached images inline or as a download link.",
1480 boolValue: 'images-inline'
1481 },{
1482 label: "Use 'contenteditable' editing mode.",
1483 boolValue: 'edit-widget-x',
1484 hint: [
1485 "When enabled, chat input uses a so-called 'contenteditable' ",
1486 "field. Though generally more comfortable and modern than ",
1487 "plain-text input fields, browser-specific quirks and bugs ",
1488 "may lead to frustration."
1489 ].join('')
1490 }];
1491
1492 /** Set up selection list of notification sounds. */
1493 if(1){
1494 const selectSound = D.select();
@@ -1465,45 +1507,64 @@
1507 selectSound.selectedIndex = firstSoundIndex;
1508 }
1509 }
1510 Chat.setNewMessageSound(selectSound.value);
1511 settingsOps.push({
1512 label: "Sound Options...",
1513 hint: "How to enable audio playback is browser-specific!",
1514 children:[{
1515 hint: "Audio alert",
1516 select: selectSound,
1517 callback: function(ev){
1518 const v = ev.target.value;
1519 Chat.setNewMessageSound(v);
1520 F.toast.message("Audio notifications "+(v ? "enabled" : "disabled")+".");
1521 if(v) setTimeout(()=>Chat.playNewMessageSound(), 0);
1522 }
1523 },{
1524 label: "Play notification for your own messages.",
1525 hint: "When enabled, the audio notification will be played for all messages, "+
1526 "including your own. When disabled only messages from other users "+
1527 "will trigger a notification.",
1528 boolValue: 'alert-own-messages'
1529 }]
1530 });
1531 }/*audio notification config*/
1532 settingsOps.push({
1533 label: "Active User List",
1534 hint: [
1535 "/chat cannot track active connections, but it can tell ",
1536 "you who has posted recently..."].join(''),
1537 children:[
1538 namedOptions.activeUsers,{
1539 label: "Timestamps in active users list",
1540 indent: true,
1541 hint: "Show most recent message timestamps in the active user list.",
1542 boolValue: 'active-user-list-timestamps'
1543 }
1544 ]
1545 });
1546 /**
1547 Build UI for config options...
1548 */
1549 settingsOps.forEach(function f(op,indentOrIndex){
1550 const line = D.addClass(D.div(), 'menu-entry');
1551 if(true===indentOrIndex) D.addClass(line, 'indent');
1552 const label = op.label
1553 ? D.append(D.label(),op.label) : undefined;
1554 const labelWrapper = D.addClass(D.div(), 'label-wrapper');
1555 var hint;
 
1556 if(op.hint){
1557 hint = D.append(D.addClass(D.span(),'hint'),op.hint);
1558 }
1559 if(op.hasOwnProperty('select')){
1560 const col0 = D.addClass(D.span(/*empty, but for spacing*/),
1561 'toggle-wrapper');
1562 D.append(line, labelWrapper, col0);
1563 D.append(labelWrapper, op.select);
1564 if(hint) D.append(labelWrapper, hint);
1565 if(label) D.append(label);
1566 if(op.callback){
1567 op.select.addEventListener('change', (ev)=>op.callback(ev), false);
1568 }
1569 }else if(op.hasOwnProperty('boolValue')){
1570 if(undefined === f.$id) f.$id = 0;
@@ -1515,23 +1576,26 @@
1576 }
1577 const check = op.checkbox
1578 = D.attr(D.checkbox(1, op.boolValue()),
1579 'aria-label', op.label);
1580 const id = 'cfgopt'+f.$id;
1581 const col0 = D.addClass(D.span(), 'toggle-wrapper');
1582 check.checked = op.boolValue();
1583 op.checkbox = check;
1584 D.attr(check, 'id', id);
1585 D.append(line, labelWrapper, col0);
1586 D.append(col0, check);
1587 if(label){
1588 D.attr(label, 'for', id);
1589 D.append(labelWrapper, label);
1590 }
1591 if(hint) D.append(labelWrapper, hint);
1592 }else{
1593 if(op.callback){
1594 line.addEventListener('click', (ev)=>op.callback(ev));
1595 }
1596 D.append(line, labelWrapper);
1597 if(label) D.append(labelWrapper, label);
1598 if(hint) D.append(labelWrapper, hint);
1599 }
1600 D.append(optionsMenu, line);
1601 if(op.persistentSetting){
@@ -1550,10 +1614,11 @@
1614 }, false);
1615 }
1616 }else if(op.callback && op.checkbox){
1617 op.checkbox.addEventListener('change', (ev)=>op.callback(ev), false);
1618 }
1619 if(op.children) op.children.forEach((x)=>f(x,true));
1620 });
1621 })()/*#chat-button-settings setup*/;
1622
1623 (function(){
1624 /* Install default settings... must come after
@@ -1569,20 +1634,58 @@
1634 Chat.settings.addListener('active-user-list-timestamps',function(s){
1635 Chat.showActiveUserTimestamps(s.value);
1636 });
1637 Chat.settings.addListener('chat-only-mode',function(s){
1638 Chat.chatOnlyMode(s.value);
1639 });
1640 Chat.settings.addListener('edit-widget-x',function(s){
1641 let eSelected;
1642 if(s.value){
1643 if(Chat.e.inputX===Chat.inputElement()) return;
1644 eSelected = Chat.e.inputX;
1645 }else{
1646 eSelected = Chat.settings.getBool('edit-compact-mode')
1647 ? Chat.e.input1 : Chat.e.inputM;
1648 }
1649 const v = Chat.inputValue();
1650 Chat.inputValue('');
1651 Chat.e.inputFields.forEach(function(e,ndx){
1652 if(eSelected===e){
1653 Chat.e.inputFields.$currentIndex = ndx;
1654 D.removeClass(e, 'hidden');
1655 }
1656 else D.addClass(e,'hidden');
1657 });
1658 Chat.inputValue(v);
1659 eSelected.focus();
1660 });
1661 Chat.settings.addListener('edit-compact-mode',function(s){
1662 if(Chat.e.inputX!==Chat.inputElement()){
1663 /* Text field/textarea mode: swap them if needed.
1664 Compact mode of inputX is toggled via CSS. */
1665 const a = s.value
1666 ? [Chat.e.input1, Chat.e.inputM, 0]
1667 : [Chat.e.inputM, Chat.e.input1, 1];
1668 const v = Chat.inputValue();
1669 Chat.inputValue('');
1670 Chat.e.inputFields.$currentIndex = a[2];
1671 Chat.inputValue(v);
1672 D.removeClass(a[0], 'hidden');
1673 D.addClass(a[1], 'hidden');
1674 }
1675 Chat.e.inputElementWrapper.classList[
1676 s.value ? 'add' : 'remove'
1677 ]('compact');
1678 Chat.e.inputFields[Chat.e.inputFields.$currentIndex].focus();
1679 });
1680 Chat.settings.addListener('edit-ctrl-send',function(s){
1681 const label = (s.value ? "Ctrl-" : "")+"Enter submits messages.";
1682 Chat.e.inputFields.forEach((e)=>{
1683 const v = e.dataset.placeholder0 + " " +label;
1684 if(e.isContentEditable) e.dataset.placeholder = v;
1685 else D.attr(e,'placeholder',v);
1686 });
1687 Chat.e.btnSubmit.title = label;
1688 });
1689 const valueKludges = {
1690 /* Convert certain string-format values to other types... */
1691 "false": false,
@@ -1607,11 +1710,11 @@
1710 this.inputFocus();
1711 };
1712 Chat.e.viewPreview.querySelector('#chat-preview-close').
1713 addEventListener('click', ()=>Chat.setCurrentView(Chat.e.viewMessages), false);
1714 let previewPending = false;
1715 const elemsToEnable = [btnPreview, Chat.e.btnSubmit, Chat.e.inputFields];
1716 const submit = function(ev){
1717 ev.preventDefault();
1718 ev.stopPropagation();
1719 if(previewPending) return false;
1720 const txt = Chat.inputValue();
1721
+74 -43
--- src/style.chat.css
+++ src/style.chat.css
@@ -40,13 +40,11 @@
4040
min-width: 9em /*avoid unsightly "underlap" with the neighboring
4141
.message-widget-tab element*/;
4242
white-space: normal;
4343
}
4444
body.chat.monospace-messages .message-widget-content,
45
-/*body.chat.monospace-messages textarea,*/
46
-/*body.chat.monospace-messages input[type=text],*/
47
-body.chat.monospace-messages #chat-input-field{
45
+body.chat.monospace-messages .chat-input-field{
4846
font-family: monospace;
4947
}
5048
body.chat .message-widget-content > * {
5149
margin: 0;
5250
padding: 0;
@@ -181,34 +179,40 @@
181179
/* Safari user reports that 2em is necessary to keep the file selection
182180
widget from overlapping the page footer, whereas a margin of 0 is fine
183181
for FF/Chrome (and 2em is a *huge* waste of space for those). */
184182
margin-bottom: 0;
185183
}
186
-#chat-input-field {
187
- display: inline-block/*supposed workaround for Chrome weirdness*/;
188
- padding: 0.2em;
184
+.chat-input-field {
189185
flex: 10 1 auto;
190
- background-color: rgba(156,156,156,0.3);
186
+ margin: 0;
187
+}
188
+#chat-input-field-x,
189
+#chat-input-field-multi {
191190
overflow: auto;
192191
resize: vertical;
192
+}
193
+#chat-input-field-x {
194
+ display: inline-block/*supposed workaround for Chrome weirdness*/;
195
+ padding: 0.2em;
196
+ background-color: rgba(156,156,156,0.3);
193197
white-space: pre-wrap;
194198
/* ^^^ Firefox, when pasting plain text into a contenteditable field,
195199
loses all newlines unless we explicitly set this. Chrome does not. */
196200
cursor: text;
197201
/* ^^^ In some browsers the cursor may not change for a contenteditable
198202
element until it has focus, causing potential confusion. */
199203
}
200
-#chat-input-field:empty::before {
204
+#chat-input-field-x:empty::before {
201205
content: attr(data-placeholder);
202206
opacity: 0.6;
203207
}
204
-#chat-input-field:not(:focus){
208
+.chat-input-field:not(:focus){
205209
border-width: 1px;
206210
border-style: solid;
207211
border-radius: 0.25em;
208212
}
209
-#chat-input-field:focus{
213
+.chat-input-field:focus{
210214
/* This transparent border helps avoid the text shifting around
211215
when the contenteditable attribute causes a border (which we
212216
apparently cannot style) to be added. */
213217
border-width: 1px;
214218
border-style: solid;
@@ -215,20 +219,20 @@
215219
border-color: transparent;
216220
border-radius: 0.25em;
217221
}
218222
/* Widget holding the chat message input field, send button, and
219223
settings button. */
220
-body.chat #chat-input-line {
224
+body.chat #chat-input-line-wrapper {
221225
display: flex;
222226
flex-direction: row;
223227
align-items: stretch;
224228
flex-wrap: nowrap;
225229
}
226
-/*body.chat #chat-input-line:not(.compact) {
230
+/*body.chat #chat-input-line-wrapper:not(.compact) {
227231
flex-wrap: nowrap;
228232
}*/
229
-body.chat #chat-input-line.compact {
233
+body.chat #chat-input-line-wrapper.compact {
230234
/* "The problem" with wrapping, together with a contenteditable input
231235
field, is that the latter grows as the user types, so causes
232236
wrapping to happen while they type, then to unwrap as soon as the
233237
input field is cleared (when the message is sent). When we stay
234238
wrapped in compact mode, the wrapped buttons simply take up too
@@ -241,11 +245,11 @@
241245
only way to eliminate the possibility that (A) the buttons
242246
get truncated in very narrow windows and (B) that they keep
243247
stable positions.
244248
*/
245249
}
246
-body.chat #chat-input-line.compact #chat-input-field {
250
+body.chat #chat-input-line-wrapper.compact #chat-input-field-x {
247251
}
248252
249253
body.chat #chat-buttons-wrapper {
250254
flex: 0 1 auto;
251255
display: flex;
@@ -255,11 +259,11 @@
255259
min-height: 1.5em;
256260
align-self: flex-end
257261
/*keep buttons stable at bottom/right even when input field
258262
resizes */;
259263
}
260
-body.chat #chat-input-line.compact #chat-buttons-wrapper {
264
+body.chat #chat-input-line-wrapper.compact #chat-buttons-wrapper {
261265
flex-direction: row;
262266
flex: 1 1 auto;
263267
align-self: stretch;
264268
justify-content: flex-end;
265269
/*flex-wrap: wrap;*/
@@ -285,28 +289,27 @@
285289
font-size: 130%;
286290
}
287291
body.chat #chat-buttons-wrapper > .cbutton:hover {
288292
background-color: rgba(200,200,200,0.3);
289293
}
290
-body.chat #chat-input-line.compact #chat-buttons-wrapper > .cbutton {
294
+body.chat #chat-input-line-wrapper.compact #chat-buttons-wrapper > .cbutton {
291295
margin: 2px 0.125em 0 0.125em;
292296
min-width: 6ex;
293297
max-width: 6ex;
294298
min-height: 2.3ex;
295299
max-height: 2.3ex;
296300
font-size: 120%;
297301
}
298
-body.chat #chat-input-line.compact #chat-buttons-wrapper #chat-button-submit {
302
+body.chat #chat-input-line-wrapper.compact #chat-buttons-wrapper #chat-button-submit {
299303
min-width: 12ex;
300304
}
301
-body.chat #chat-input-line:not(.compact) #chat-input-field {
302
- /*border-left-style: double;
303
- border-left-width: 3px;
304
- border-right-style: double;
305
- border-right-width: 3px;*/
305
+.chat-input-field {
306
+ font-family: inherit
307
+}
308
+body.chat #chat-input-line-wrapper:not(.compact) #chat-input-field-multi,
309
+body.chat #chat-input-line-wrapper:not(.compact) #chat-input-field-x {
306310
min-height: 4rem;
307
- /*max-height: 50rem;*/
308311
/*
309312
Problems related to max-height:
310313
311314
- If we do NOT set a max-height then pasting/typing a large amount
312315
of text can cause this element to grow without bounds, larger than
@@ -317,25 +320,25 @@
317320
318321
- If we DO set a max-height then its growth is bounded but it also
319322
cannot manually expanded by the user.
320323
321324
The lesser of the two evils seems to be to rely on the browser
322
- feature that a manual resize of the element will pin its sits.
325
+ feature that a manual resize of the element will pin its size.
323326
*/
324327
}
325328
326
-body.chat #chat-input-line > #chat-button-settings{
329
+body.chat #chat-input-line-wrapper > #chat-button-settings{
327330
margin: 0 0 0 0.25em;
328331
max-width: 2em;
329332
}
330
-body.chat #chat-input-line > input[type=text],
331
-body.chat #chat-input-line > textarea {
333
+body.chat #chat-input-line-wrapper > input[type=text],
334
+body.chat #chat-input-line-wrapper > textarea {
332335
flex: 20 1 auto;
333336
max-width: revert;
334337
min-width: 20em;
335338
}
336
-body.chat #chat-input-line.compact > input[type=text] {
339
+body.chat #chat-input-line-wrapper.compact > input[type=text] {
337340
margin: 0 0 0.25em 0/* gap for if/when buttons wrap*/;
338341
}
339342
/* Widget holding the file selection control and preview */
340343
body.chat #chat-input-file-area {
341344
display: flex;
@@ -367,11 +370,14 @@
367370
white-space: pre;
368371
font-family: monospace;
369372
margin: auto;
370373
flex: 0;
371374
}
372
-
375
+body.chat #chat-drop-details:empty {
376
+ padding: 0;
377
+ margin: 0;
378
+}
373379
body.chat #chat-drop-details img {
374380
max-width: 45%;
375381
max-height: 45%;
376382
}
377383
body.chat .chat-view {
@@ -395,37 +401,62 @@
395401
/* /chat config options go here */
396402
flex: 1 1 auto;
397403
display: flex;
398404
flex-direction: column;
399405
overflow: auto;
406
+ align-items: stretch;
400407
}
401408
body.chat #chat-config #chat-config-options .menu-entry {
402409
display: flex;
403
- align-items: baseline;
410
+ align-items: center;
404411
flex-direction: row;
405412
flex-wrap: nowrap;
406413
padding: 1em;
414
+ flex: 1 1 auto;
415
+ align-self: stretch;
416
+}
417
+body.chat #chat-config #chat-config-options .menu-entry.indent {
418
+ padding-left: 2.5em;
419
+}
420
+body.chat #chat-config #chat-config-options .menu-entry:nth-of-type(even){
421
+ background-color: rgba(175,175,175,0.1);
422
+}
423
+body.chat #chat-config #chat-config-options .menu-entry:nth-of-type(odd){
424
+ background-color: rgba(175,175,175,0.25);
425
+}
426
+body.chat #chat-config #chat-config-options .menu-entry:first-child {
427
+ /* Config list header */
428
+}
429
+body.chat #chat-config #chat-config-options .menu-entry:first-child .label-wrapper {
430
+ align-items: start;
431
+}
432
+body.chat #chat-config #chat-config-options .menu-entry > .toggle-wrapper {
433
+ /* Holder for a checkbox, if any */
434
+ min-width: 1.5rem;
435
+ margin-left: 1rem;
436
+}
437
+body.chat #chat-config #chat-config-options .menu-entry .label-wrapper {
438
+ /* Wrapper for a LABEL and a .hint element. */
439
+ display: flex;
440
+ flex-direction: column;
441
+ align-self: baseline;
442
+ flex: 1 1 auto;
443
+}
444
+body.chat #chat-config #chat-config-options .menu-entry label {
445
+ /* Config option label. */
446
+ font-weight: bold;
447
+ white-space: initial;
407448
}
408449
body.chat #chat-config #chat-config-options .menu-entry label[for] {
409450
cursor: pointer;
410451
}
411
-body.chat #chat-config #chat-config-options .menu-entry > *:first-child {
412
- min-width: 1.5rem;
413
-}
414
-body.chat #chat-config #chat-config-options .menu-entry span.hint {
452
+body.chat #chat-config #chat-config-options .menu-entry .hint {
415453
/* Config menu hint text */
416
- font-size: 80%;
454
+ font-size: 85%;
417455
white-space: pre-wrap;
418456
display: inline-block;
419
-}
420
-body.chat #chat-config #chat-config-options .menu-entry:first-child {
421
-}
422
-body.chat #chat-config #chat-config-options .menu-entry div.label-wrapper {
423
- display: flex;
424
- flex-direction: column;
425
- align-self: baseline;
426
- margin-left: 1em;
457
+ opacity: 0.85;
427458
}
428459
body.chat #chat-config #chat-config-options .menu-entry select {
429460
}
430461
body.chat #chat-preview #chat-preview-content {
431462
overflow: auto;
432463
--- src/style.chat.css
+++ src/style.chat.css
@@ -40,13 +40,11 @@
40 min-width: 9em /*avoid unsightly "underlap" with the neighboring
41 .message-widget-tab element*/;
42 white-space: normal;
43 }
44 body.chat.monospace-messages .message-widget-content,
45 /*body.chat.monospace-messages textarea,*/
46 /*body.chat.monospace-messages input[type=text],*/
47 body.chat.monospace-messages #chat-input-field{
48 font-family: monospace;
49 }
50 body.chat .message-widget-content > * {
51 margin: 0;
52 padding: 0;
@@ -181,34 +179,40 @@
181 /* Safari user reports that 2em is necessary to keep the file selection
182 widget from overlapping the page footer, whereas a margin of 0 is fine
183 for FF/Chrome (and 2em is a *huge* waste of space for those). */
184 margin-bottom: 0;
185 }
186 #chat-input-field {
187 display: inline-block/*supposed workaround for Chrome weirdness*/;
188 padding: 0.2em;
189 flex: 10 1 auto;
190 background-color: rgba(156,156,156,0.3);
 
 
 
191 overflow: auto;
192 resize: vertical;
 
 
 
 
 
193 white-space: pre-wrap;
194 /* ^^^ Firefox, when pasting plain text into a contenteditable field,
195 loses all newlines unless we explicitly set this. Chrome does not. */
196 cursor: text;
197 /* ^^^ In some browsers the cursor may not change for a contenteditable
198 element until it has focus, causing potential confusion. */
199 }
200 #chat-input-field:empty::before {
201 content: attr(data-placeholder);
202 opacity: 0.6;
203 }
204 #chat-input-field:not(:focus){
205 border-width: 1px;
206 border-style: solid;
207 border-radius: 0.25em;
208 }
209 #chat-input-field:focus{
210 /* This transparent border helps avoid the text shifting around
211 when the contenteditable attribute causes a border (which we
212 apparently cannot style) to be added. */
213 border-width: 1px;
214 border-style: solid;
@@ -215,20 +219,20 @@
215 border-color: transparent;
216 border-radius: 0.25em;
217 }
218 /* Widget holding the chat message input field, send button, and
219 settings button. */
220 body.chat #chat-input-line {
221 display: flex;
222 flex-direction: row;
223 align-items: stretch;
224 flex-wrap: nowrap;
225 }
226 /*body.chat #chat-input-line:not(.compact) {
227 flex-wrap: nowrap;
228 }*/
229 body.chat #chat-input-line.compact {
230 /* "The problem" with wrapping, together with a contenteditable input
231 field, is that the latter grows as the user types, so causes
232 wrapping to happen while they type, then to unwrap as soon as the
233 input field is cleared (when the message is sent). When we stay
234 wrapped in compact mode, the wrapped buttons simply take up too
@@ -241,11 +245,11 @@
241 only way to eliminate the possibility that (A) the buttons
242 get truncated in very narrow windows and (B) that they keep
243 stable positions.
244 */
245 }
246 body.chat #chat-input-line.compact #chat-input-field {
247 }
248
249 body.chat #chat-buttons-wrapper {
250 flex: 0 1 auto;
251 display: flex;
@@ -255,11 +259,11 @@
255 min-height: 1.5em;
256 align-self: flex-end
257 /*keep buttons stable at bottom/right even when input field
258 resizes */;
259 }
260 body.chat #chat-input-line.compact #chat-buttons-wrapper {
261 flex-direction: row;
262 flex: 1 1 auto;
263 align-self: stretch;
264 justify-content: flex-end;
265 /*flex-wrap: wrap;*/
@@ -285,28 +289,27 @@
285 font-size: 130%;
286 }
287 body.chat #chat-buttons-wrapper > .cbutton:hover {
288 background-color: rgba(200,200,200,0.3);
289 }
290 body.chat #chat-input-line.compact #chat-buttons-wrapper > .cbutton {
291 margin: 2px 0.125em 0 0.125em;
292 min-width: 6ex;
293 max-width: 6ex;
294 min-height: 2.3ex;
295 max-height: 2.3ex;
296 font-size: 120%;
297 }
298 body.chat #chat-input-line.compact #chat-buttons-wrapper #chat-button-submit {
299 min-width: 12ex;
300 }
301 body.chat #chat-input-line:not(.compact) #chat-input-field {
302 /*border-left-style: double;
303 border-left-width: 3px;
304 border-right-style: double;
305 border-right-width: 3px;*/
306 min-height: 4rem;
307 /*max-height: 50rem;*/
308 /*
309 Problems related to max-height:
310
311 - If we do NOT set a max-height then pasting/typing a large amount
312 of text can cause this element to grow without bounds, larger than
@@ -317,25 +320,25 @@
317
318 - If we DO set a max-height then its growth is bounded but it also
319 cannot manually expanded by the user.
320
321 The lesser of the two evils seems to be to rely on the browser
322 feature that a manual resize of the element will pin its sits.
323 */
324 }
325
326 body.chat #chat-input-line > #chat-button-settings{
327 margin: 0 0 0 0.25em;
328 max-width: 2em;
329 }
330 body.chat #chat-input-line > input[type=text],
331 body.chat #chat-input-line > textarea {
332 flex: 20 1 auto;
333 max-width: revert;
334 min-width: 20em;
335 }
336 body.chat #chat-input-line.compact > input[type=text] {
337 margin: 0 0 0.25em 0/* gap for if/when buttons wrap*/;
338 }
339 /* Widget holding the file selection control and preview */
340 body.chat #chat-input-file-area {
341 display: flex;
@@ -367,11 +370,14 @@
367 white-space: pre;
368 font-family: monospace;
369 margin: auto;
370 flex: 0;
371 }
372
 
 
 
373 body.chat #chat-drop-details img {
374 max-width: 45%;
375 max-height: 45%;
376 }
377 body.chat .chat-view {
@@ -395,37 +401,62 @@
395 /* /chat config options go here */
396 flex: 1 1 auto;
397 display: flex;
398 flex-direction: column;
399 overflow: auto;
 
400 }
401 body.chat #chat-config #chat-config-options .menu-entry {
402 display: flex;
403 align-items: baseline;
404 flex-direction: row;
405 flex-wrap: nowrap;
406 padding: 1em;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407 }
408 body.chat #chat-config #chat-config-options .menu-entry label[for] {
409 cursor: pointer;
410 }
411 body.chat #chat-config #chat-config-options .menu-entry > *:first-child {
412 min-width: 1.5rem;
413 }
414 body.chat #chat-config #chat-config-options .menu-entry span.hint {
415 /* Config menu hint text */
416 font-size: 80%;
417 white-space: pre-wrap;
418 display: inline-block;
419 }
420 body.chat #chat-config #chat-config-options .menu-entry:first-child {
421 }
422 body.chat #chat-config #chat-config-options .menu-entry div.label-wrapper {
423 display: flex;
424 flex-direction: column;
425 align-self: baseline;
426 margin-left: 1em;
427 }
428 body.chat #chat-config #chat-config-options .menu-entry select {
429 }
430 body.chat #chat-preview #chat-preview-content {
431 overflow: auto;
432
--- src/style.chat.css
+++ src/style.chat.css
@@ -40,13 +40,11 @@
40 min-width: 9em /*avoid unsightly "underlap" with the neighboring
41 .message-widget-tab element*/;
42 white-space: normal;
43 }
44 body.chat.monospace-messages .message-widget-content,
45 body.chat.monospace-messages .chat-input-field{
 
 
46 font-family: monospace;
47 }
48 body.chat .message-widget-content > * {
49 margin: 0;
50 padding: 0;
@@ -181,34 +179,40 @@
179 /* Safari user reports that 2em is necessary to keep the file selection
180 widget from overlapping the page footer, whereas a margin of 0 is fine
181 for FF/Chrome (and 2em is a *huge* waste of space for those). */
182 margin-bottom: 0;
183 }
184 .chat-input-field {
 
 
185 flex: 10 1 auto;
186 margin: 0;
187 }
188 #chat-input-field-x,
189 #chat-input-field-multi {
190 overflow: auto;
191 resize: vertical;
192 }
193 #chat-input-field-x {
194 display: inline-block/*supposed workaround for Chrome weirdness*/;
195 padding: 0.2em;
196 background-color: rgba(156,156,156,0.3);
197 white-space: pre-wrap;
198 /* ^^^ Firefox, when pasting plain text into a contenteditable field,
199 loses all newlines unless we explicitly set this. Chrome does not. */
200 cursor: text;
201 /* ^^^ In some browsers the cursor may not change for a contenteditable
202 element until it has focus, causing potential confusion. */
203 }
204 #chat-input-field-x:empty::before {
205 content: attr(data-placeholder);
206 opacity: 0.6;
207 }
208 .chat-input-field:not(:focus){
209 border-width: 1px;
210 border-style: solid;
211 border-radius: 0.25em;
212 }
213 .chat-input-field:focus{
214 /* This transparent border helps avoid the text shifting around
215 when the contenteditable attribute causes a border (which we
216 apparently cannot style) to be added. */
217 border-width: 1px;
218 border-style: solid;
@@ -215,20 +219,20 @@
219 border-color: transparent;
220 border-radius: 0.25em;
221 }
222 /* Widget holding the chat message input field, send button, and
223 settings button. */
224 body.chat #chat-input-line-wrapper {
225 display: flex;
226 flex-direction: row;
227 align-items: stretch;
228 flex-wrap: nowrap;
229 }
230 /*body.chat #chat-input-line-wrapper:not(.compact) {
231 flex-wrap: nowrap;
232 }*/
233 body.chat #chat-input-line-wrapper.compact {
234 /* "The problem" with wrapping, together with a contenteditable input
235 field, is that the latter grows as the user types, so causes
236 wrapping to happen while they type, then to unwrap as soon as the
237 input field is cleared (when the message is sent). When we stay
238 wrapped in compact mode, the wrapped buttons simply take up too
@@ -241,11 +245,11 @@
245 only way to eliminate the possibility that (A) the buttons
246 get truncated in very narrow windows and (B) that they keep
247 stable positions.
248 */
249 }
250 body.chat #chat-input-line-wrapper.compact #chat-input-field-x {
251 }
252
253 body.chat #chat-buttons-wrapper {
254 flex: 0 1 auto;
255 display: flex;
@@ -255,11 +259,11 @@
259 min-height: 1.5em;
260 align-self: flex-end
261 /*keep buttons stable at bottom/right even when input field
262 resizes */;
263 }
264 body.chat #chat-input-line-wrapper.compact #chat-buttons-wrapper {
265 flex-direction: row;
266 flex: 1 1 auto;
267 align-self: stretch;
268 justify-content: flex-end;
269 /*flex-wrap: wrap;*/
@@ -285,28 +289,27 @@
289 font-size: 130%;
290 }
291 body.chat #chat-buttons-wrapper > .cbutton:hover {
292 background-color: rgba(200,200,200,0.3);
293 }
294 body.chat #chat-input-line-wrapper.compact #chat-buttons-wrapper > .cbutton {
295 margin: 2px 0.125em 0 0.125em;
296 min-width: 6ex;
297 max-width: 6ex;
298 min-height: 2.3ex;
299 max-height: 2.3ex;
300 font-size: 120%;
301 }
302 body.chat #chat-input-line-wrapper.compact #chat-buttons-wrapper #chat-button-submit {
303 min-width: 12ex;
304 }
305 .chat-input-field {
306 font-family: inherit
307 }
308 body.chat #chat-input-line-wrapper:not(.compact) #chat-input-field-multi,
309 body.chat #chat-input-line-wrapper:not(.compact) #chat-input-field-x {
310 min-height: 4rem;
 
311 /*
312 Problems related to max-height:
313
314 - If we do NOT set a max-height then pasting/typing a large amount
315 of text can cause this element to grow without bounds, larger than
@@ -317,25 +320,25 @@
320
321 - If we DO set a max-height then its growth is bounded but it also
322 cannot manually expanded by the user.
323
324 The lesser of the two evils seems to be to rely on the browser
325 feature that a manual resize of the element will pin its size.
326 */
327 }
328
329 body.chat #chat-input-line-wrapper > #chat-button-settings{
330 margin: 0 0 0 0.25em;
331 max-width: 2em;
332 }
333 body.chat #chat-input-line-wrapper > input[type=text],
334 body.chat #chat-input-line-wrapper > textarea {
335 flex: 20 1 auto;
336 max-width: revert;
337 min-width: 20em;
338 }
339 body.chat #chat-input-line-wrapper.compact > input[type=text] {
340 margin: 0 0 0.25em 0/* gap for if/when buttons wrap*/;
341 }
342 /* Widget holding the file selection control and preview */
343 body.chat #chat-input-file-area {
344 display: flex;
@@ -367,11 +370,14 @@
370 white-space: pre;
371 font-family: monospace;
372 margin: auto;
373 flex: 0;
374 }
375 body.chat #chat-drop-details:empty {
376 padding: 0;
377 margin: 0;
378 }
379 body.chat #chat-drop-details img {
380 max-width: 45%;
381 max-height: 45%;
382 }
383 body.chat .chat-view {
@@ -395,37 +401,62 @@
401 /* /chat config options go here */
402 flex: 1 1 auto;
403 display: flex;
404 flex-direction: column;
405 overflow: auto;
406 align-items: stretch;
407 }
408 body.chat #chat-config #chat-config-options .menu-entry {
409 display: flex;
410 align-items: center;
411 flex-direction: row;
412 flex-wrap: nowrap;
413 padding: 1em;
414 flex: 1 1 auto;
415 align-self: stretch;
416 }
417 body.chat #chat-config #chat-config-options .menu-entry.indent {
418 padding-left: 2.5em;
419 }
420 body.chat #chat-config #chat-config-options .menu-entry:nth-of-type(even){
421 background-color: rgba(175,175,175,0.1);
422 }
423 body.chat #chat-config #chat-config-options .menu-entry:nth-of-type(odd){
424 background-color: rgba(175,175,175,0.25);
425 }
426 body.chat #chat-config #chat-config-options .menu-entry:first-child {
427 /* Config list header */
428 }
429 body.chat #chat-config #chat-config-options .menu-entry:first-child .label-wrapper {
430 align-items: start;
431 }
432 body.chat #chat-config #chat-config-options .menu-entry > .toggle-wrapper {
433 /* Holder for a checkbox, if any */
434 min-width: 1.5rem;
435 margin-left: 1rem;
436 }
437 body.chat #chat-config #chat-config-options .menu-entry .label-wrapper {
438 /* Wrapper for a LABEL and a .hint element. */
439 display: flex;
440 flex-direction: column;
441 align-self: baseline;
442 flex: 1 1 auto;
443 }
444 body.chat #chat-config #chat-config-options .menu-entry label {
445 /* Config option label. */
446 font-weight: bold;
447 white-space: initial;
448 }
449 body.chat #chat-config #chat-config-options .menu-entry label[for] {
450 cursor: pointer;
451 }
452 body.chat #chat-config #chat-config-options .menu-entry .hint {
 
 
 
453 /* Config menu hint text */
454 font-size: 85%;
455 white-space: pre-wrap;
456 display: inline-block;
457 opacity: 0.85;
 
 
 
 
 
 
 
458 }
459 body.chat #chat-config #chat-config-options .menu-entry select {
460 }
461 body.chat #chat-preview #chat-preview-content {
462 overflow: auto;
463
--- www/changes.wiki
+++ www/changes.wiki
@@ -1,6 +1,10 @@
11
<title>Change Log</title>
2
+
3
+<h2 id='v2_18'>Changes for version 2.18 (pending)</h2>
4
+ * [/help?cmd=/chat|The /chat page] input options have been reworked
5
+ again for better cross-browser portability.
26
37
<h2 id='v2_17'>Changes for version 2.17 (2021-10-09)</h2>
48
59
* Major improvements to the "diff" subsystem, including: <ul>
610
<li> Added new [/help?cmd=diff|formatting options]: --by, -b, --webpage, --json, --tcl.
711
--- www/changes.wiki
+++ www/changes.wiki
@@ -1,6 +1,10 @@
1 <title>Change Log</title>
 
 
 
 
2
3 <h2 id='v2_17'>Changes for version 2.17 (2021-10-09)</h2>
4
5 * Major improvements to the "diff" subsystem, including: <ul>
6 <li> Added new [/help?cmd=diff|formatting options]: --by, -b, --webpage, --json, --tcl.
7
--- www/changes.wiki
+++ www/changes.wiki
@@ -1,6 +1,10 @@
1 <title>Change Log</title>
2
3 <h2 id='v2_18'>Changes for version 2.18 (pending)</h2>
4 * [/help?cmd=/chat|The /chat page] input options have been reworked
5 again for better cross-browser portability.
6
7 <h2 id='v2_17'>Changes for version 2.17 (2021-10-09)</h2>
8
9 * Major improvements to the "diff" subsystem, including: <ul>
10 <li> Added new [/help?cmd=diff|formatting options]: --by, -b, --webpage, --json, --tcl.
11
--- www/changes.wiki
+++ www/changes.wiki
@@ -1,6 +1,10 @@
11
<title>Change Log</title>
2
+
3
+<h2 id='v2_18'>Changes for version 2.18 (pending)</h2>
4
+ * [/help?cmd=/chat|The /chat page] input options have been reworked
5
+ again for better cross-browser portability.
26
37
<h2 id='v2_17'>Changes for version 2.17 (2021-10-09)</h2>
48
59
* Major improvements to the "diff" subsystem, including: <ul>
610
<li> Added new [/help?cmd=diff|formatting options]: --by, -b, --webpage, --json, --tcl.
711
--- www/changes.wiki
+++ www/changes.wiki
@@ -1,6 +1,10 @@
1 <title>Change Log</title>
 
 
 
 
2
3 <h2 id='v2_17'>Changes for version 2.17 (2021-10-09)</h2>
4
5 * Major improvements to the "diff" subsystem, including: <ul>
6 <li> Added new [/help?cmd=diff|formatting options]: --by, -b, --webpage, --json, --tcl.
7
--- www/changes.wiki
+++ www/changes.wiki
@@ -1,6 +1,10 @@
1 <title>Change Log</title>
2
3 <h2 id='v2_18'>Changes for version 2.18 (pending)</h2>
4 * [/help?cmd=/chat|The /chat page] input options have been reworked
5 again for better cross-browser portability.
6
7 <h2 id='v2_17'>Changes for version 2.17 (2021-10-09)</h2>
8
9 * Major improvements to the "diff" subsystem, including: <ul>
10 <li> Added new [/help?cmd=diff|formatting options]: --by, -b, --webpage, --json, --tcl.
11

Keyboard Shortcuts

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