Index: src/chat.c ================================================================== --- src/chat.c +++ src/chat.c @@ -174,15 +174,23 @@ zInputPlaceholder0 = mprintf("Type markdown-formatted message for %h.", zProjectName); style_set_current_feature("chat"); style_header("Chat"); @
- @
- @
+ @ + @ + @
+ @ class="chat-input-field hidden">
@
@ 👁 @ %s(zPaperclip) Index: src/fossil.page.chat.js ================================================================== --- src/fossil.page.chat.js +++ src/fossil.page.chat.js @@ -94,11 +94,11 @@ ht = wh; }else{ elemsToCount.forEach((e)=>e ? extra += D.effectiveHeight(e) : false); ht = wh - extra; } - f.chat.e.inputField.style.maxHeight = (ht/2)+"px"; + f.chat.e.inputX.style.maxHeight = (ht/2)+"px"; /* ^^^^ this is a middle ground between having no size cap on the input field and having a fixed arbitrary cap. */; contentArea.style.height = contentArea.style.maxHeight = [ "calc(", (ht>=100 ? ht : 100), "px", @@ -110,11 +110,11 @@ if(false){ console.debug("resized.",wh, extra, ht, window.getComputedStyle(contentArea).maxHeight, contentArea); console.debug("Set input max height to: ", - f.chat.e.inputField.style.maxHeight); + f.chat.e.inputX.style.maxHeight); } }; var doit; window.addEventListener('resize',function(ev){ clearTimeout(doit); @@ -131,16 +131,18 @@ e:{/*map of certain DOM elements.*/ messageInjectPoint: E1('#message-inject-point'), pageTitle: E1('head title'), loadOlderToolbar: undefined /* the load-posts toolbar (dynamically created) */, inputWrapper: E1("#chat-input-area"), - inputLine: E1('#chat-input-line'), + inputElementWrapper: E1('#chat-input-line-wrapper'), fileSelectWrapper: E1('#chat-input-file-area'), viewMessages: E1('#chat-messages-wrapper'), btnSubmit: E1('#chat-button-submit'), btnAttach: E1('#chat-button-attach'), - inputField: E1('#chat-input-field'), + inputX: E1('#chat-input-field-x'), + input1: E1('#chat-input-field-single'), + inputM: E1('#chat-input-field-multi'), inputFile: E1('#chat-input-file'), contentDiv: E1('div.content'), viewConfig: E1('#chat-config'), viewPreview: E1('#chat-preview'), previewContent: E1('#chat-preview-content'), @@ -176,23 +178,24 @@ taking into account single- vs multi-line input. The getter returns a string and the setter returns this object. */ inputValue: function(){ const e = this.inputElement(); if(arguments.length){ - e.innerText = arguments[0]; + if(e.isContentEditable) e.innerText = arguments[0]; + else e.value = arguments[0]; return this; } - return e.innerText; + return e.isContentEditable ? e.innerText : e.value; }, /** Asks the current user input field to take focus. Returns this. */ inputFocus: function(){ this.inputElement().focus(); return this; }, /** Returns the current message input element. */ inputElement: function(){ - return this.e.inputField; + return this.e.inputFields[this.e.inputFields.$currentIndex]; }, /** Enables (if yes is truthy) or disables all elements in * this.disableDuringAjax. */ enableAjaxComponents: function(yes){ D[yes ? 'enable' : 'disable'](this.disableDuringAjax); @@ -417,11 +420,17 @@ timestamp of each user's most recent message. */ "active-user-list-timestamps": false, /* When on, the [audible-alert] is played for one's own messages, else it is only played for other users' messages. */ - "alert-own-messages": false + "alert-own-messages": false, + /* "Experimental mode" input: use a contenteditable field + for input. This is generally more comfortable to use, + and more modern, than plain text input fields, but + the list of browser-specific quirks and bugs is... + not short. */ + "edit-widget-x": false } }, /** Plays a new-message notification sound IF the audible-alert setting is true, else this is a no-op. Returns this. */ @@ -579,16 +588,22 @@ D.addClassBriefly(e, a, 0, cb); } return this; } }; - if(D.attr(cs.e.inputField,'contenteditable','plaintext-only').isContentEditable){ + cs.e.inputFields = [ cs.e.input1, cs.e.inputM, cs.e.inputX ]; + cs.e.inputFields.$currentIndex = 0; + cs.e.inputFields.forEach(function(e,ndx){ + if(ndx===cs.e.inputFields.$currentIndex) D.removeClass(e,'hidden'); + else D.addClass(e,'hidden'); + }); + if(D.attr(cs.e.inputX,'contenteditable','plaintext-only').isContentEditable){ cs.$browserHasPlaintextOnly = true; }else{ /* Only the Chrome family supports contenteditable=plaintext-only */ cs.$browserHasPlaintextOnly = false; - D.attr(cs.e.inputField,'contenteditable','true'); + D.attr(cs.e.inputX,'contenteditable','true'); } cs.animate.$disabled = true; F.fetch.beforesend = ()=>cs.ajaxStart(); F.fetch.aftersend = ()=>cs.ajaxEnd(); cs.pageTitleOrig = cs.e.pageTitle.innerText; @@ -1161,11 +1176,11 @@ /* Acrobatics to keep *some* installations of Firefox from pasting formatting into contenteditable fields. This also works on Chrome, but chrome has the contenteditable=plaintext-only property which does this for us. */ - Chat.inputElement().addEventListener( + Chat.e.inputX.addEventListener( 'paste', function(ev){ if (ev.clipboardData && ev.clipboardData.getData) { const pastedText = ev.clipboardData.getData('text/plain'); const selection = window.getSelection(); @@ -1185,15 +1200,12 @@ ev.dataTransfer.dropEffect = 'none'; ev.preventDefault(); ev.stopPropagation(); return false; }; - ['drop','dragenter','dragleave','dragend'].forEach( - (k)=>{ - Chat.inputElement().addEventListener(k, noDragDropEvents, false); - } + (k)=>Chat.e.inputX.addEventListener(k, noDragDropEvents, false) ); return bxs; })()/*drag/drop/paste*/; const tzOffsetToString = function(off){ @@ -1323,11 +1335,13 @@ ev.stopPropagation(); Chat.submitMessage(); return false; } }; - Chat.e.inputField.addEventListener('keydown', inputWidgetKeydown, false); + Chat.e.inputFields.forEach( + (e)=>e.addEventListener('keydown', inputWidgetKeydown, false) + ); Chat.e.btnSubmit.addEventListener('click',(e)=>{ e.preventDefault(); Chat.submitMessage(); return false; }); @@ -1383,70 +1397,98 @@ /** Settings ops structure: label: string for the UI - boolValue: string (name of Chat.settings setting) or a - function which returns true or false. + boolValue: string (name of Chat.settings setting) or a function + which returns true or false. If it is a string, it gets + replaced by a function which returns + Chat.settings.getBool(thatString) and the string gets assigned + to the persistentSetting property of this object. select: SELECT element (instead of boolValue) callback: optional handler to call after setting is modified. + Its "this" is the options object. If this object has a + boolValue string or a persistentSetting property, the argument + passed to the callback is a settings object in the form {key:K, + value:V}. If this object does not have boolValue string or + persistentSetting then the callback is passed an event object + in response to the config option's UI widget being activated, + normally a 'change' event. + + children: [array of settings objects]. These get listed under + this element and indented slightly for visual grouping. Only + one level of indention is supported. + + Elements which only have a label and maybe a hint and + children can be used as headings. - If a setting has a boolValue set, that gets transformed into a + If a setting has a boolValue set, that gets rendered as a checkbox which toggles the given persistent setting (if boolValue is a string) AND listens for changes to that setting fired via Chat.settings.set() so that the checkbox can stay in sync with external changes to that setting. Various Chat UI elements stay in sync with the config UI via those settings - events. - */ + events. The checkbox element gets added to the options object + so that the callback() can reference it via this.checkbox. + */ const settingsOps = [{ + label: "Chat Configuration Options", + hint: "Most of these settings are persistent via window.localStorage." + },{ + label: "Chat-only mode", + hint: "Toggle the page between normal fossil view and chat-only view.", + boolValue: 'chat-only-mode' + },{ label: "Ctrl-enter to Send", - hint: "When on, only Ctrl-Enter will send messages and Enter adds "+ - "blank lines. "+ - "When off, both Enter and Ctrl-Enter send. "+ - "When the input field has focus, is empty, and preview "+ - "mode is NOT active then Ctrl-Enter toggles this setting.", + hint: [ + "When on, only Ctrl-Enter will send messages and Enter adds ", + "blank lines. When off, both Enter and Ctrl-Enter send. ", + "When the input field has focus, is empty, and preview ", + "mode is NOT active then Ctrl-Enter toggles this setting." + ].join(''), boolValue: 'edit-ctrl-send' },{ label: "Compact mode", - hint: "Toggle between a space-saving or more spacious writing area. "+ - "When the input field has focus, is empty, and preview mode "+ - "is NOT active then Shift-Enter toggles this setting.", + hint: [ + "Toggle between a space-saving or more spacious writing area. ", + "When the input field has focus, is empty, and preview mode ", + "is NOT active then Shift-Enter toggles this setting."].join(''), boolValue: 'edit-compact-mode' },{ label: "Left-align my posts", hint: "Default alignment of your own messages is selected " - +"based window width/height relationship.", + + "based window width/height relationship.", boolValue: ()=>!document.body.classList.contains('my-messages-right'), callback: function f(){ document.body.classList[ this.checkbox.checked ? 'remove' : 'add' ]('my-messages-right'); } },{ label: "Monospace message font", - hint: "Use monospace font for message text?", + hint: "Use monospace font for message and input text.", boolValue: 'monospace-messages', callback: function(setting){ document.body.classList[ setting.value ? 'add' : 'remove' ]('monospace-messages'); } },{ - label: "Chat-only mode", - hint: "Toggle the page between normal fossil view and chat-only view.", - boolValue: 'chat-only-mode' + label: "Show images inline", + hint: "Show attached images inline or as a download link.", + boolValue: 'images-inline' },{ - label: "Show images inline", - hint: "Whether to show images inline or as a hyperlink.", - boolValue: 'images-inline' - },namedOptions.activeUsers,{ - label: "Timestamps in active users list", - hint: "Whether to show last-message timestamps.", - boolValue: 'active-user-list-timestamps' + label: "Use 'contenteditable' editing mode.", + boolValue: 'edit-widget-x', + hint: [ + "When enabled, chat input uses a so-called 'contenteditable' ", + "field. Though generally more comfortable and modern than ", + "plain-text input fields, browser-specific quirks and bugs ", + "may lead to frustration." + ].join('') }]; /** Set up selection list of notification sounds. */ if(1){ const selectSound = D.select(); @@ -1465,45 +1507,64 @@ selectSound.selectedIndex = firstSoundIndex; } } Chat.setNewMessageSound(selectSound.value); settingsOps.push({ - hint: "Audio alert. How to enable audio playback is browser-specific!", - select: selectSound, - callback: function(ev){ - const v = ev.target.value; - Chat.setNewMessageSound(v); - F.toast.message("Audio notifications "+(v ? "enabled" : "disabled")+"."); - if(v) setTimeout(()=>Chat.playNewMessageSound(), 0); - } + label: "Sound Options...", + hint: "How to enable audio playback is browser-specific!", + children:[{ + hint: "Audio alert", + select: selectSound, + callback: function(ev){ + const v = ev.target.value; + Chat.setNewMessageSound(v); + F.toast.message("Audio notifications "+(v ? "enabled" : "disabled")+"."); + if(v) setTimeout(()=>Chat.playNewMessageSound(), 0); + } + },{ + label: "Play notification for your own messages.", + hint: "When enabled, the audio notification will be played for all messages, "+ + "including your own. When disabled only messages from other users "+ + "will trigger a notification.", + boolValue: 'alert-own-messages' + }] }); }/*audio notification config*/ settingsOps.push({ - label: "Play notification for your own messages.", - hint: "When enabled, the audio notification will be played for all messages, "+ - "including your own. When disabled only messages from other users "+ - "will trigger a notification.", - boolValue: 'alert-own-messages' + label: "Active User List", + hint: [ + "/chat cannot track active connections, but it can tell ", + "you who has posted recently..."].join(''), + children:[ + namedOptions.activeUsers,{ + label: "Timestamps in active users list", + indent: true, + hint: "Show most recent message timestamps in the active user list.", + boolValue: 'active-user-list-timestamps' + } + ] }); /** Build UI for config options... */ - settingsOps.forEach(function f(op){ + settingsOps.forEach(function f(op,indentOrIndex){ const line = D.addClass(D.div(), 'menu-entry'); + if(true===indentOrIndex) D.addClass(line, 'indent'); const label = op.label ? D.append(D.label(),op.label) : undefined; const labelWrapper = D.addClass(D.div(), 'label-wrapper'); var hint; - const col0 = D.span(); if(op.hint){ hint = D.append(D.addClass(D.span(),'hint'),op.hint); } if(op.hasOwnProperty('select')){ - D.append(line, col0, labelWrapper); + const col0 = D.addClass(D.span(/*empty, but for spacing*/), + 'toggle-wrapper'); + D.append(line, labelWrapper, col0); D.append(labelWrapper, op.select); if(hint) D.append(labelWrapper, hint); - if(label) D.append(col0, label); + if(label) D.append(label); if(op.callback){ op.select.addEventListener('change', (ev)=>op.callback(ev), false); } }else if(op.hasOwnProperty('boolValue')){ if(undefined === f.$id) f.$id = 0; @@ -1515,23 +1576,26 @@ } const check = op.checkbox = D.attr(D.checkbox(1, op.boolValue()), 'aria-label', op.label); const id = 'cfgopt'+f.$id; + const col0 = D.addClass(D.span(), 'toggle-wrapper'); check.checked = op.boolValue(); op.checkbox = check; D.attr(check, 'id', id); - D.append(line, col0, labelWrapper); + D.append(line, labelWrapper, col0); D.append(col0, check); if(label){ D.attr(label, 'for', id); D.append(labelWrapper, label); } if(hint) D.append(labelWrapper, hint); }else{ - line.addEventListener('click', callback); - D.append(line, col0, labelWrapper); + if(op.callback){ + line.addEventListener('click', (ev)=>op.callback(ev)); + } + D.append(line, labelWrapper); if(label) D.append(labelWrapper, label); if(hint) D.append(labelWrapper, hint); } D.append(optionsMenu, line); if(op.persistentSetting){ @@ -1550,10 +1614,11 @@ }, false); } }else if(op.callback && op.checkbox){ op.checkbox.addEventListener('change', (ev)=>op.callback(ev), false); } + if(op.children) op.children.forEach((x)=>f(x,true)); }); })()/*#chat-button-settings setup*/; (function(){ /* Install default settings... must come after @@ -1569,20 +1634,58 @@ Chat.settings.addListener('active-user-list-timestamps',function(s){ Chat.showActiveUserTimestamps(s.value); }); Chat.settings.addListener('chat-only-mode',function(s){ Chat.chatOnlyMode(s.value); + }); + Chat.settings.addListener('edit-widget-x',function(s){ + let eSelected; + if(s.value){ + if(Chat.e.inputX===Chat.inputElement()) return; + eSelected = Chat.e.inputX; + }else{ + eSelected = Chat.settings.getBool('edit-compact-mode') + ? Chat.e.input1 : Chat.e.inputM; + } + const v = Chat.inputValue(); + Chat.inputValue(''); + Chat.e.inputFields.forEach(function(e,ndx){ + if(eSelected===e){ + Chat.e.inputFields.$currentIndex = ndx; + D.removeClass(e, 'hidden'); + } + else D.addClass(e,'hidden'); + }); + Chat.inputValue(v); + eSelected.focus(); }); Chat.settings.addListener('edit-compact-mode',function(s){ - Chat.e.inputLine.classList[ + if(Chat.e.inputX!==Chat.inputElement()){ + /* Text field/textarea mode: swap them if needed. + Compact mode of inputX is toggled via CSS. */ + const a = s.value + ? [Chat.e.input1, Chat.e.inputM, 0] + : [Chat.e.inputM, Chat.e.input1, 1]; + const v = Chat.inputValue(); + Chat.inputValue(''); + Chat.e.inputFields.$currentIndex = a[2]; + Chat.inputValue(v); + D.removeClass(a[0], 'hidden'); + D.addClass(a[1], 'hidden'); + } + Chat.e.inputElementWrapper.classList[ s.value ? 'add' : 'remove' ]('compact'); + Chat.e.inputFields[Chat.e.inputFields.$currentIndex].focus(); }); Chat.settings.addListener('edit-ctrl-send',function(s){ const label = (s.value ? "Ctrl-" : "")+"Enter submits messages."; - const eInput = Chat.inputElement(); - eInput.dataset.placeholder = eInput.dataset.placeholder0 + " " +label; + Chat.e.inputFields.forEach((e)=>{ + const v = e.dataset.placeholder0 + " " +label; + if(e.isContentEditable) e.dataset.placeholder = v; + else D.attr(e,'placeholder',v); + }); Chat.e.btnSubmit.title = label; }); const valueKludges = { /* Convert certain string-format values to other types... */ "false": false, @@ -1607,11 +1710,11 @@ this.inputFocus(); }; Chat.e.viewPreview.querySelector('#chat-preview-close'). addEventListener('click', ()=>Chat.setCurrentView(Chat.e.viewMessages), false); let previewPending = false; - const elemsToEnable = [btnPreview, Chat.e.btnSubmit, Chat.e.inputField]; + const elemsToEnable = [btnPreview, Chat.e.btnSubmit, Chat.e.inputFields]; const submit = function(ev){ ev.preventDefault(); ev.stopPropagation(); if(previewPending) return false; const txt = Chat.inputValue(); Index: src/style.chat.css ================================================================== --- src/style.chat.css +++ src/style.chat.css @@ -40,13 +40,11 @@ min-width: 9em /*avoid unsightly "underlap" with the neighboring .message-widget-tab element*/; white-space: normal; } body.chat.monospace-messages .message-widget-content, -/*body.chat.monospace-messages textarea,*/ -/*body.chat.monospace-messages input[type=text],*/ -body.chat.monospace-messages #chat-input-field{ +body.chat.monospace-messages .chat-input-field{ font-family: monospace; } body.chat .message-widget-content > * { margin: 0; padding: 0; @@ -181,34 +179,40 @@ /* Safari user reports that 2em is necessary to keep the file selection widget from overlapping the page footer, whereas a margin of 0 is fine for FF/Chrome (and 2em is a *huge* waste of space for those). */ margin-bottom: 0; } -#chat-input-field { - display: inline-block/*supposed workaround for Chrome weirdness*/; - padding: 0.2em; +.chat-input-field { flex: 10 1 auto; - background-color: rgba(156,156,156,0.3); + margin: 0; +} +#chat-input-field-x, +#chat-input-field-multi { overflow: auto; resize: vertical; +} +#chat-input-field-x { + display: inline-block/*supposed workaround for Chrome weirdness*/; + padding: 0.2em; + background-color: rgba(156,156,156,0.3); white-space: pre-wrap; /* ^^^ Firefox, when pasting plain text into a contenteditable field, loses all newlines unless we explicitly set this. Chrome does not. */ cursor: text; /* ^^^ In some browsers the cursor may not change for a contenteditable element until it has focus, causing potential confusion. */ } -#chat-input-field:empty::before { +#chat-input-field-x:empty::before { content: attr(data-placeholder); opacity: 0.6; } -#chat-input-field:not(:focus){ +.chat-input-field:not(:focus){ border-width: 1px; border-style: solid; border-radius: 0.25em; } -#chat-input-field:focus{ +.chat-input-field:focus{ /* This transparent border helps avoid the text shifting around when the contenteditable attribute causes a border (which we apparently cannot style) to be added. */ border-width: 1px; border-style: solid; @@ -215,20 +219,20 @@ border-color: transparent; border-radius: 0.25em; } /* Widget holding the chat message input field, send button, and settings button. */ -body.chat #chat-input-line { +body.chat #chat-input-line-wrapper { display: flex; flex-direction: row; align-items: stretch; flex-wrap: nowrap; } -/*body.chat #chat-input-line:not(.compact) { +/*body.chat #chat-input-line-wrapper:not(.compact) { flex-wrap: nowrap; }*/ -body.chat #chat-input-line.compact { +body.chat #chat-input-line-wrapper.compact { /* "The problem" with wrapping, together with a contenteditable input field, is that the latter grows as the user types, so causes wrapping to happen while they type, then to unwrap as soon as the input field is cleared (when the message is sent). When we stay wrapped in compact mode, the wrapped buttons simply take up too @@ -241,11 +245,11 @@ only way to eliminate the possibility that (A) the buttons get truncated in very narrow windows and (B) that they keep stable positions. */ } -body.chat #chat-input-line.compact #chat-input-field { +body.chat #chat-input-line-wrapper.compact #chat-input-field-x { } body.chat #chat-buttons-wrapper { flex: 0 1 auto; display: flex; @@ -255,11 +259,11 @@ min-height: 1.5em; align-self: flex-end /*keep buttons stable at bottom/right even when input field resizes */; } -body.chat #chat-input-line.compact #chat-buttons-wrapper { +body.chat #chat-input-line-wrapper.compact #chat-buttons-wrapper { flex-direction: row; flex: 1 1 auto; align-self: stretch; justify-content: flex-end; /*flex-wrap: wrap;*/ @@ -285,28 +289,27 @@ font-size: 130%; } body.chat #chat-buttons-wrapper > .cbutton:hover { background-color: rgba(200,200,200,0.3); } -body.chat #chat-input-line.compact #chat-buttons-wrapper > .cbutton { +body.chat #chat-input-line-wrapper.compact #chat-buttons-wrapper > .cbutton { margin: 2px 0.125em 0 0.125em; min-width: 6ex; max-width: 6ex; min-height: 2.3ex; max-height: 2.3ex; font-size: 120%; } -body.chat #chat-input-line.compact #chat-buttons-wrapper #chat-button-submit { +body.chat #chat-input-line-wrapper.compact #chat-buttons-wrapper #chat-button-submit { min-width: 12ex; } -body.chat #chat-input-line:not(.compact) #chat-input-field { - /*border-left-style: double; - border-left-width: 3px; - border-right-style: double; - border-right-width: 3px;*/ +.chat-input-field { + font-family: inherit +} +body.chat #chat-input-line-wrapper:not(.compact) #chat-input-field-multi, +body.chat #chat-input-line-wrapper:not(.compact) #chat-input-field-x { min-height: 4rem; - /*max-height: 50rem;*/ /* Problems related to max-height: - If we do NOT set a max-height then pasting/typing a large amount of text can cause this element to grow without bounds, larger than @@ -317,25 +320,25 @@ - If we DO set a max-height then its growth is bounded but it also cannot manually expanded by the user. The lesser of the two evils seems to be to rely on the browser - feature that a manual resize of the element will pin its sits. + feature that a manual resize of the element will pin its size. */ } -body.chat #chat-input-line > #chat-button-settings{ +body.chat #chat-input-line-wrapper > #chat-button-settings{ margin: 0 0 0 0.25em; max-width: 2em; } -body.chat #chat-input-line > input[type=text], -body.chat #chat-input-line > textarea { +body.chat #chat-input-line-wrapper > input[type=text], +body.chat #chat-input-line-wrapper > textarea { flex: 20 1 auto; max-width: revert; min-width: 20em; } -body.chat #chat-input-line.compact > input[type=text] { +body.chat #chat-input-line-wrapper.compact > input[type=text] { margin: 0 0 0.25em 0/* gap for if/when buttons wrap*/; } /* Widget holding the file selection control and preview */ body.chat #chat-input-file-area { display: flex; @@ -367,11 +370,14 @@ white-space: pre; font-family: monospace; margin: auto; flex: 0; } - +body.chat #chat-drop-details:empty { + padding: 0; + margin: 0; +} body.chat #chat-drop-details img { max-width: 45%; max-height: 45%; } body.chat .chat-view { @@ -395,37 +401,62 @@ /* /chat config options go here */ flex: 1 1 auto; display: flex; flex-direction: column; overflow: auto; + align-items: stretch; } body.chat #chat-config #chat-config-options .menu-entry { display: flex; - align-items: baseline; + align-items: center; flex-direction: row; flex-wrap: nowrap; padding: 1em; + flex: 1 1 auto; + align-self: stretch; +} +body.chat #chat-config #chat-config-options .menu-entry.indent { + padding-left: 2.5em; +} +body.chat #chat-config #chat-config-options .menu-entry:nth-of-type(even){ + background-color: rgba(175,175,175,0.1); +} +body.chat #chat-config #chat-config-options .menu-entry:nth-of-type(odd){ + background-color: rgba(175,175,175,0.25); +} +body.chat #chat-config #chat-config-options .menu-entry:first-child { + /* Config list header */ +} +body.chat #chat-config #chat-config-options .menu-entry:first-child .label-wrapper { + align-items: start; +} +body.chat #chat-config #chat-config-options .menu-entry > .toggle-wrapper { + /* Holder for a checkbox, if any */ + min-width: 1.5rem; + margin-left: 1rem; +} +body.chat #chat-config #chat-config-options .menu-entry .label-wrapper { + /* Wrapper for a LABEL and a .hint element. */ + display: flex; + flex-direction: column; + align-self: baseline; + flex: 1 1 auto; +} +body.chat #chat-config #chat-config-options .menu-entry label { + /* Config option label. */ + font-weight: bold; + white-space: initial; } body.chat #chat-config #chat-config-options .menu-entry label[for] { cursor: pointer; } -body.chat #chat-config #chat-config-options .menu-entry > *:first-child { - min-width: 1.5rem; -} -body.chat #chat-config #chat-config-options .menu-entry span.hint { +body.chat #chat-config #chat-config-options .menu-entry .hint { /* Config menu hint text */ - font-size: 80%; + font-size: 85%; white-space: pre-wrap; display: inline-block; -} -body.chat #chat-config #chat-config-options .menu-entry:first-child { -} -body.chat #chat-config #chat-config-options .menu-entry div.label-wrapper { - display: flex; - flex-direction: column; - align-self: baseline; - margin-left: 1em; + opacity: 0.85; } body.chat #chat-config #chat-config-options .menu-entry select { } body.chat #chat-preview #chat-preview-content { overflow: auto; Index: www/changes.wiki ================================================================== --- www/changes.wiki +++ www/changes.wiki @@ -1,6 +1,10 @@ Change Log + +

Changes for version 2.18 (pending)

+ * [/help?cmd=/chat|The /chat page] input options have been reworked + again for better cross-browser portability.

Changes for version 2.17 (pending)

* Major improvements to the "diff" subsystem, including:
  • Added new [/help?cmd=diff|formatting options]: --by, -b, --webpage, --json, --tcl.
  • Partial-line matching for unified diffs