Fossil SCM
/chat: added option to toggle between text and contenteditable widget, defaulting to the former. Prettied up the config view a bit and made it more right-handed friendly.
Commit
5d7c98ef92046b068c663327b9cf12187654fdbf1e835c68860da3ea561ea9fc
Parent
136d95b6f172d9b…
2 files changed
+96
-34
+45
-17
+96
-34
| --- src/fossil.page.chat.js | ||
| +++ src/fossil.page.chat.js | ||
| @@ -420,11 +420,17 @@ | ||
| 420 | 420 | timestamp of each user's most recent message. */ |
| 421 | 421 | "active-user-list-timestamps": false, |
| 422 | 422 | /* When on, the [audible-alert] is played for one's own |
| 423 | 423 | messages, else it is only played for other users' |
| 424 | 424 | messages. */ |
| 425 | - "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 | |
| 426 | 432 | } |
| 427 | 433 | }, |
| 428 | 434 | /** Plays a new-message notification sound IF the audible-alert |
| 429 | 435 | setting is true, else this is a no-op. Returns this. |
| 430 | 436 | */ |
| @@ -1391,70 +1397,97 @@ | ||
| 1391 | 1397 | /** |
| 1392 | 1398 | Settings ops structure: |
| 1393 | 1399 | |
| 1394 | 1400 | label: string for the UI |
| 1395 | 1401 | |
| 1396 | - boolValue: string (name of Chat.settings setting) or a | |
| 1397 | - 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. | |
| 1398 | 1407 | |
| 1399 | 1408 | select: SELECT element (instead of boolValue) |
| 1400 | 1409 | |
| 1401 | 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. | |
| 1402 | 1418 | |
| 1403 | - If a setting has a boolValue set, that gets transformed into a | |
| 1419 | + If a setting has a boolValue set, that gets rendered as a | |
| 1404 | 1420 | checkbox which toggles the given persistent setting (if |
| 1405 | 1421 | boolValue is a string) AND listens for changes to that setting |
| 1406 | 1422 | fired via Chat.settings.set() so that the checkbox can stay in |
| 1407 | 1423 | sync with external changes to that setting. Various Chat UI |
| 1408 | 1424 | elements stay in sync with the config UI via those settings |
| 1409 | - events. | |
| 1410 | - */ | |
| 1425 | + events. The checkbox element gets added to the options object | |
| 1426 | + so that the callback() can reference it via this.checkbox. | |
| 1427 | + */ | |
| 1411 | 1428 | const settingsOps = [{ |
| 1429 | + label: "Chat Configuration Options", | |
| 1430 | + hint: "Most of these settings are persistent via window.localStorage." | |
| 1431 | + },{ | |
| 1432 | + label: "Chat-only mode", | |
| 1433 | + hint: "Toggle the page between normal fossil view and chat-only view.", | |
| 1434 | + boolValue: 'chat-only-mode' | |
| 1435 | + },{ | |
| 1412 | 1436 | label: "Ctrl-enter to Send", |
| 1413 | - hint: "When on, only Ctrl-Enter will send messages and Enter adds "+ | |
| 1414 | - "blank lines. "+ | |
| 1415 | - "When off, both Enter and Ctrl-Enter send. "+ | |
| 1416 | - "When the input field has focus, is empty, and preview "+ | |
| 1417 | - "mode is NOT active then Ctrl-Enter toggles this setting.", | |
| 1437 | + hint: [ | |
| 1438 | + "When on, only Ctrl-Enter will send messages and Enter adds ", | |
| 1439 | + "blank lines. When off, both Enter and Ctrl-Enter send. ", | |
| 1440 | + "When the input field has focus, is empty, and preview ", | |
| 1441 | + "mode is NOT active then Ctrl-Enter toggles this setting." | |
| 1442 | + ].join(''), | |
| 1418 | 1443 | boolValue: 'edit-ctrl-send' |
| 1419 | 1444 | },{ |
| 1420 | 1445 | label: "Compact mode", |
| 1421 | - hint: "Toggle between a space-saving or more spacious writing area. "+ | |
| 1422 | - "When the input field has focus, is empty, and preview mode "+ | |
| 1423 | - "is NOT active then Shift-Enter toggles this setting.", | |
| 1446 | + hint: [ | |
| 1447 | + "Toggle between a space-saving or more spacious writing area. ", | |
| 1448 | + "When the input field has focus, is empty, and preview mode ", | |
| 1449 | + "is NOT active then Shift-Enter toggles this setting."].join(''), | |
| 1424 | 1450 | boolValue: 'edit-compact-mode' |
| 1425 | 1451 | },{ |
| 1426 | 1452 | label: "Left-align my posts", |
| 1427 | 1453 | hint: "Default alignment of your own messages is selected " |
| 1428 | - +"based window width/height relationship.", | |
| 1454 | + + "based window width/height relationship.", | |
| 1429 | 1455 | boolValue: ()=>!document.body.classList.contains('my-messages-right'), |
| 1430 | 1456 | callback: function f(){ |
| 1431 | 1457 | document.body.classList[ |
| 1432 | 1458 | this.checkbox.checked ? 'remove' : 'add' |
| 1433 | 1459 | ]('my-messages-right'); |
| 1434 | 1460 | } |
| 1435 | 1461 | },{ |
| 1436 | 1462 | label: "Monospace message font", |
| 1437 | - hint: "Use monospace font for message text?", | |
| 1463 | + hint: "Use monospace font for message and input text.", | |
| 1438 | 1464 | boolValue: 'monospace-messages', |
| 1439 | 1465 | callback: function(setting){ |
| 1440 | 1466 | document.body.classList[ |
| 1441 | 1467 | setting.value ? 'add' : 'remove' |
| 1442 | 1468 | ]('monospace-messages'); |
| 1443 | 1469 | } |
| 1444 | 1470 | },{ |
| 1445 | - label: "Chat-only mode", | |
| 1446 | - hint: "Toggle the page between normal fossil view and chat-only view.", | |
| 1447 | - boolValue: 'chat-only-mode' | |
| 1448 | - },{ | |
| 1449 | 1471 | label: "Show images inline", |
| 1450 | - hint: "Whether to show images inline or as a hyperlink.", | |
| 1472 | + hint: "Show attached images inline or as a download link.", | |
| 1451 | 1473 | boolValue: 'images-inline' |
| 1452 | - },namedOptions.activeUsers,{ | |
| 1474 | + }, | |
| 1475 | + namedOptions.activeUsers, | |
| 1476 | + { | |
| 1453 | 1477 | label: "Timestamps in active users list", |
| 1454 | - hint: "Whether to show last-message timestamps.", | |
| 1478 | + hint: "Show most recent message timestamps in the active user list.", | |
| 1455 | 1479 | boolValue: 'active-user-list-timestamps' |
| 1480 | + },{ | |
| 1481 | + label: "Use 'contenteditable' editing mode.", | |
| 1482 | + boolValue: 'edit-widget-x', | |
| 1483 | + hint: [ | |
| 1484 | + "When enabled, chat input uses a so-called 'contenteditable' ", | |
| 1485 | + "field. Though generally more comfortable and modern than ", | |
| 1486 | + "plain-text input fields, browser-specific quirks and bugs ", | |
| 1487 | + "may lead to frustration." | |
| 1488 | + ].join('') | |
| 1456 | 1489 | }]; |
| 1457 | 1490 | |
| 1458 | 1491 | /** Set up selection list of notification sounds. */ |
| 1459 | 1492 | if(1){ |
| 1460 | 1493 | const selectSound = D.select(); |
| @@ -1473,11 +1506,14 @@ | ||
| 1473 | 1506 | selectSound.selectedIndex = firstSoundIndex; |
| 1474 | 1507 | } |
| 1475 | 1508 | } |
| 1476 | 1509 | Chat.setNewMessageSound(selectSound.value); |
| 1477 | 1510 | settingsOps.push({ |
| 1478 | - hint: "Audio alert. How to enable audio playback is browser-specific!", | |
| 1511 | + label: "Sound Options...", | |
| 1512 | + hint: "How to enable audio playback is browser-specific!" | |
| 1513 | + },{ | |
| 1514 | + hint: "Audio alert", | |
| 1479 | 1515 | select: selectSound, |
| 1480 | 1516 | callback: function(ev){ |
| 1481 | 1517 | const v = ev.target.value; |
| 1482 | 1518 | Chat.setNewMessageSound(v); |
| 1483 | 1519 | F.toast.message("Audio notifications "+(v ? "enabled" : "disabled")+"."); |
| @@ -1499,19 +1535,20 @@ | ||
| 1499 | 1535 | const line = D.addClass(D.div(), 'menu-entry'); |
| 1500 | 1536 | const label = op.label |
| 1501 | 1537 | ? D.append(D.label(),op.label) : undefined; |
| 1502 | 1538 | const labelWrapper = D.addClass(D.div(), 'label-wrapper'); |
| 1503 | 1539 | var hint; |
| 1504 | - const col0 = D.span(); | |
| 1505 | 1540 | if(op.hint){ |
| 1506 | 1541 | hint = D.append(D.addClass(D.span(),'hint'),op.hint); |
| 1507 | 1542 | } |
| 1508 | 1543 | if(op.hasOwnProperty('select')){ |
| 1509 | - D.append(line, col0, labelWrapper); | |
| 1544 | + const col0 = D.addClass(D.span(/*empty, but for spacing*/), | |
| 1545 | + 'toggle-wrapper'); | |
| 1546 | + D.append(line, labelWrapper, col0); | |
| 1510 | 1547 | D.append(labelWrapper, op.select); |
| 1511 | 1548 | if(hint) D.append(labelWrapper, hint); |
| 1512 | - if(label) D.append(col0, label); | |
| 1549 | + if(label) D.append(label); | |
| 1513 | 1550 | if(op.callback){ |
| 1514 | 1551 | op.select.addEventListener('change', (ev)=>op.callback(ev), false); |
| 1515 | 1552 | } |
| 1516 | 1553 | }else if(op.hasOwnProperty('boolValue')){ |
| 1517 | 1554 | if(undefined === f.$id) f.$id = 0; |
| @@ -1523,23 +1560,26 @@ | ||
| 1523 | 1560 | } |
| 1524 | 1561 | const check = op.checkbox |
| 1525 | 1562 | = D.attr(D.checkbox(1, op.boolValue()), |
| 1526 | 1563 | 'aria-label', op.label); |
| 1527 | 1564 | const id = 'cfgopt'+f.$id; |
| 1565 | + const col0 = D.addClass(D.span(), 'toggle-wrapper'); | |
| 1528 | 1566 | check.checked = op.boolValue(); |
| 1529 | 1567 | op.checkbox = check; |
| 1530 | 1568 | D.attr(check, 'id', id); |
| 1531 | - D.append(line, col0, labelWrapper); | |
| 1569 | + D.append(line, labelWrapper, col0); | |
| 1532 | 1570 | D.append(col0, check); |
| 1533 | 1571 | if(label){ |
| 1534 | 1572 | D.attr(label, 'for', id); |
| 1535 | 1573 | D.append(labelWrapper, label); |
| 1536 | 1574 | } |
| 1537 | 1575 | if(hint) D.append(labelWrapper, hint); |
| 1538 | 1576 | }else{ |
| 1539 | - line.addEventListener('click', callback); | |
| 1540 | - D.append(line, col0, labelWrapper); | |
| 1577 | + if(op.callback){ | |
| 1578 | + line.addEventListener('click', (ev)=>op.callback(ev)); | |
| 1579 | + } | |
| 1580 | + D.append(line, labelWrapper); | |
| 1541 | 1581 | if(label) D.append(labelWrapper, label); |
| 1542 | 1582 | if(hint) D.append(labelWrapper, hint); |
| 1543 | 1583 | } |
| 1544 | 1584 | D.append(optionsMenu, line); |
| 1545 | 1585 | if(op.persistentSetting){ |
| @@ -1577,28 +1617,50 @@ | ||
| 1577 | 1617 | Chat.settings.addListener('active-user-list-timestamps',function(s){ |
| 1578 | 1618 | Chat.showActiveUserTimestamps(s.value); |
| 1579 | 1619 | }); |
| 1580 | 1620 | Chat.settings.addListener('chat-only-mode',function(s){ |
| 1581 | 1621 | Chat.chatOnlyMode(s.value); |
| 1622 | + }); | |
| 1623 | + Chat.settings.addListener('edit-widget-x',function(s){ | |
| 1624 | + let eSelected; | |
| 1625 | + if(s.value){ | |
| 1626 | + if(Chat.e.inputX===Chat.inputElement()) return; | |
| 1627 | + eSelected = Chat.e.inputX; | |
| 1628 | + }else{ | |
| 1629 | + eSelected = Chat.settings.getBool('edit-compact-mode') | |
| 1630 | + ? Chat.e.input1 : Chat.e.inputM; | |
| 1631 | + } | |
| 1632 | + const v = Chat.inputValue(); | |
| 1633 | + Chat.inputValue(''); | |
| 1634 | + Chat.e.inputFields.forEach(function(e,ndx){ | |
| 1635 | + if(eSelected===e){ | |
| 1636 | + Chat.e.inputFields.$currentIndex = ndx; | |
| 1637 | + D.removeClass(e, 'hidden'); | |
| 1638 | + } | |
| 1639 | + else D.addClass(e,'hidden'); | |
| 1640 | + }); | |
| 1641 | + Chat.inputValue(v); | |
| 1642 | + eSelected.focus(); | |
| 1582 | 1643 | }); |
| 1583 | 1644 | Chat.settings.addListener('edit-compact-mode',function(s){ |
| 1584 | 1645 | if(Chat.e.inputX!==Chat.inputElement()){ |
| 1585 | - /* text field/textarea mode: swap them if needed. */ | |
| 1646 | + /* Text field/textarea mode: swap them if needed. | |
| 1647 | + Compact mode of inputX is toggled via CSS. */ | |
| 1586 | 1648 | const a = s.value |
| 1587 | 1649 | ? [Chat.e.input1, Chat.e.inputM, 0] |
| 1588 | 1650 | : [Chat.e.inputM, Chat.e.input1, 1]; |
| 1589 | 1651 | const v = Chat.inputValue(); |
| 1590 | 1652 | Chat.inputValue(''); |
| 1653 | + Chat.e.inputFields.$currentIndex = a[2]; | |
| 1654 | + Chat.inputValue(v); | |
| 1591 | 1655 | D.removeClass(a[0], 'hidden'); |
| 1592 | 1656 | D.addClass(a[1], 'hidden'); |
| 1593 | - Chat.e.inputFields.$currentIndex = a[2]; | |
| 1594 | - Chat.inputValue(v); | |
| 1595 | - a[0].focus(); | |
| 1596 | 1657 | } |
| 1597 | 1658 | Chat.e.inputElementWrapper.classList[ |
| 1598 | 1659 | s.value ? 'add' : 'remove' |
| 1599 | 1660 | ]('compact'); |
| 1661 | + Chat.e.inputFields[Chat.e.inputFields.$currentIndex].focus(); | |
| 1600 | 1662 | }); |
| 1601 | 1663 | Chat.settings.addListener('edit-ctrl-send',function(s){ |
| 1602 | 1664 | const label = (s.value ? "Ctrl-" : "")+"Enter submits messages."; |
| 1603 | 1665 | Chat.e.inputFields.forEach((e)=>{ |
| 1604 | 1666 | const v = e.dataset.placeholder0 + " " +label; |
| 1605 | 1667 |
| --- src/fossil.page.chat.js | |
| +++ src/fossil.page.chat.js | |
| @@ -420,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 | } |
| 427 | }, |
| 428 | /** Plays a new-message notification sound IF the audible-alert |
| 429 | setting is true, else this is a no-op. Returns this. |
| 430 | */ |
| @@ -1391,70 +1397,97 @@ | |
| 1391 | /** |
| 1392 | Settings ops structure: |
| 1393 | |
| 1394 | label: string for the UI |
| 1395 | |
| 1396 | boolValue: string (name of Chat.settings setting) or a |
| 1397 | function which returns true or false. |
| 1398 | |
| 1399 | select: SELECT element (instead of boolValue) |
| 1400 | |
| 1401 | callback: optional handler to call after setting is modified. |
| 1402 | |
| 1403 | If a setting has a boolValue set, that gets transformed into a |
| 1404 | checkbox which toggles the given persistent setting (if |
| 1405 | boolValue is a string) AND listens for changes to that setting |
| 1406 | fired via Chat.settings.set() so that the checkbox can stay in |
| 1407 | sync with external changes to that setting. Various Chat UI |
| 1408 | elements stay in sync with the config UI via those settings |
| 1409 | events. |
| 1410 | */ |
| 1411 | const settingsOps = [{ |
| 1412 | label: "Ctrl-enter to Send", |
| 1413 | hint: "When on, only Ctrl-Enter will send messages and Enter adds "+ |
| 1414 | "blank lines. "+ |
| 1415 | "When off, both Enter and Ctrl-Enter send. "+ |
| 1416 | "When the input field has focus, is empty, and preview "+ |
| 1417 | "mode is NOT active then Ctrl-Enter toggles this setting.", |
| 1418 | boolValue: 'edit-ctrl-send' |
| 1419 | },{ |
| 1420 | label: "Compact mode", |
| 1421 | hint: "Toggle between a space-saving or more spacious writing area. "+ |
| 1422 | "When the input field has focus, is empty, and preview mode "+ |
| 1423 | "is NOT active then Shift-Enter toggles this setting.", |
| 1424 | boolValue: 'edit-compact-mode' |
| 1425 | },{ |
| 1426 | label: "Left-align my posts", |
| 1427 | hint: "Default alignment of your own messages is selected " |
| 1428 | +"based window width/height relationship.", |
| 1429 | boolValue: ()=>!document.body.classList.contains('my-messages-right'), |
| 1430 | callback: function f(){ |
| 1431 | document.body.classList[ |
| 1432 | this.checkbox.checked ? 'remove' : 'add' |
| 1433 | ]('my-messages-right'); |
| 1434 | } |
| 1435 | },{ |
| 1436 | label: "Monospace message font", |
| 1437 | hint: "Use monospace font for message text?", |
| 1438 | boolValue: 'monospace-messages', |
| 1439 | callback: function(setting){ |
| 1440 | document.body.classList[ |
| 1441 | setting.value ? 'add' : 'remove' |
| 1442 | ]('monospace-messages'); |
| 1443 | } |
| 1444 | },{ |
| 1445 | label: "Chat-only mode", |
| 1446 | hint: "Toggle the page between normal fossil view and chat-only view.", |
| 1447 | boolValue: 'chat-only-mode' |
| 1448 | },{ |
| 1449 | label: "Show images inline", |
| 1450 | hint: "Whether to show images inline or as a hyperlink.", |
| 1451 | boolValue: 'images-inline' |
| 1452 | },namedOptions.activeUsers,{ |
| 1453 | label: "Timestamps in active users list", |
| 1454 | hint: "Whether to show last-message timestamps.", |
| 1455 | boolValue: 'active-user-list-timestamps' |
| 1456 | }]; |
| 1457 | |
| 1458 | /** Set up selection list of notification sounds. */ |
| 1459 | if(1){ |
| 1460 | const selectSound = D.select(); |
| @@ -1473,11 +1506,14 @@ | |
| 1473 | selectSound.selectedIndex = firstSoundIndex; |
| 1474 | } |
| 1475 | } |
| 1476 | Chat.setNewMessageSound(selectSound.value); |
| 1477 | settingsOps.push({ |
| 1478 | hint: "Audio alert. How to enable audio playback is browser-specific!", |
| 1479 | select: selectSound, |
| 1480 | callback: function(ev){ |
| 1481 | const v = ev.target.value; |
| 1482 | Chat.setNewMessageSound(v); |
| 1483 | F.toast.message("Audio notifications "+(v ? "enabled" : "disabled")+"."); |
| @@ -1499,19 +1535,20 @@ | |
| 1499 | const line = D.addClass(D.div(), 'menu-entry'); |
| 1500 | const label = op.label |
| 1501 | ? D.append(D.label(),op.label) : undefined; |
| 1502 | const labelWrapper = D.addClass(D.div(), 'label-wrapper'); |
| 1503 | var hint; |
| 1504 | const col0 = D.span(); |
| 1505 | if(op.hint){ |
| 1506 | hint = D.append(D.addClass(D.span(),'hint'),op.hint); |
| 1507 | } |
| 1508 | if(op.hasOwnProperty('select')){ |
| 1509 | D.append(line, col0, labelWrapper); |
| 1510 | D.append(labelWrapper, op.select); |
| 1511 | if(hint) D.append(labelWrapper, hint); |
| 1512 | if(label) D.append(col0, label); |
| 1513 | if(op.callback){ |
| 1514 | op.select.addEventListener('change', (ev)=>op.callback(ev), false); |
| 1515 | } |
| 1516 | }else if(op.hasOwnProperty('boolValue')){ |
| 1517 | if(undefined === f.$id) f.$id = 0; |
| @@ -1523,23 +1560,26 @@ | |
| 1523 | } |
| 1524 | const check = op.checkbox |
| 1525 | = D.attr(D.checkbox(1, op.boolValue()), |
| 1526 | 'aria-label', op.label); |
| 1527 | const id = 'cfgopt'+f.$id; |
| 1528 | check.checked = op.boolValue(); |
| 1529 | op.checkbox = check; |
| 1530 | D.attr(check, 'id', id); |
| 1531 | D.append(line, col0, labelWrapper); |
| 1532 | D.append(col0, check); |
| 1533 | if(label){ |
| 1534 | D.attr(label, 'for', id); |
| 1535 | D.append(labelWrapper, label); |
| 1536 | } |
| 1537 | if(hint) D.append(labelWrapper, hint); |
| 1538 | }else{ |
| 1539 | line.addEventListener('click', callback); |
| 1540 | D.append(line, col0, labelWrapper); |
| 1541 | if(label) D.append(labelWrapper, label); |
| 1542 | if(hint) D.append(labelWrapper, hint); |
| 1543 | } |
| 1544 | D.append(optionsMenu, line); |
| 1545 | if(op.persistentSetting){ |
| @@ -1577,28 +1617,50 @@ | |
| 1577 | Chat.settings.addListener('active-user-list-timestamps',function(s){ |
| 1578 | Chat.showActiveUserTimestamps(s.value); |
| 1579 | }); |
| 1580 | Chat.settings.addListener('chat-only-mode',function(s){ |
| 1581 | Chat.chatOnlyMode(s.value); |
| 1582 | }); |
| 1583 | Chat.settings.addListener('edit-compact-mode',function(s){ |
| 1584 | if(Chat.e.inputX!==Chat.inputElement()){ |
| 1585 | /* text field/textarea mode: swap them if needed. */ |
| 1586 | const a = s.value |
| 1587 | ? [Chat.e.input1, Chat.e.inputM, 0] |
| 1588 | : [Chat.e.inputM, Chat.e.input1, 1]; |
| 1589 | const v = Chat.inputValue(); |
| 1590 | Chat.inputValue(''); |
| 1591 | D.removeClass(a[0], 'hidden'); |
| 1592 | D.addClass(a[1], 'hidden'); |
| 1593 | Chat.e.inputFields.$currentIndex = a[2]; |
| 1594 | Chat.inputValue(v); |
| 1595 | a[0].focus(); |
| 1596 | } |
| 1597 | Chat.e.inputElementWrapper.classList[ |
| 1598 | s.value ? 'add' : 'remove' |
| 1599 | ]('compact'); |
| 1600 | }); |
| 1601 | Chat.settings.addListener('edit-ctrl-send',function(s){ |
| 1602 | const label = (s.value ? "Ctrl-" : "")+"Enter submits messages."; |
| 1603 | Chat.e.inputFields.forEach((e)=>{ |
| 1604 | const v = e.dataset.placeholder0 + " " +label; |
| 1605 |
| --- src/fossil.page.chat.js | |
| +++ src/fossil.page.chat.js | |
| @@ -420,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 | */ |
| @@ -1391,70 +1397,97 @@ | |
| 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 | If a setting has a boolValue set, that gets rendered as a |
| 1420 | checkbox which toggles the given persistent setting (if |
| 1421 | boolValue is a string) AND listens for changes to that setting |
| 1422 | fired via Chat.settings.set() so that the checkbox can stay in |
| 1423 | sync with external changes to that setting. Various Chat UI |
| 1424 | elements stay in sync with the config UI via those settings |
| 1425 | events. The checkbox element gets added to the options object |
| 1426 | so that the callback() can reference it via this.checkbox. |
| 1427 | */ |
| 1428 | const settingsOps = [{ |
| 1429 | label: "Chat Configuration Options", |
| 1430 | hint: "Most of these settings are persistent via window.localStorage." |
| 1431 | },{ |
| 1432 | label: "Chat-only mode", |
| 1433 | hint: "Toggle the page between normal fossil view and chat-only view.", |
| 1434 | boolValue: 'chat-only-mode' |
| 1435 | },{ |
| 1436 | label: "Ctrl-enter to Send", |
| 1437 | hint: [ |
| 1438 | "When on, only Ctrl-Enter will send messages and Enter adds ", |
| 1439 | "blank lines. When off, both Enter and Ctrl-Enter send. ", |
| 1440 | "When the input field has focus, is empty, and preview ", |
| 1441 | "mode is NOT active then Ctrl-Enter toggles this setting." |
| 1442 | ].join(''), |
| 1443 | boolValue: 'edit-ctrl-send' |
| 1444 | },{ |
| 1445 | label: "Compact mode", |
| 1446 | hint: [ |
| 1447 | "Toggle between a space-saving or more spacious writing area. ", |
| 1448 | "When the input field has focus, is empty, and preview mode ", |
| 1449 | "is NOT active then Shift-Enter toggles this setting."].join(''), |
| 1450 | boolValue: 'edit-compact-mode' |
| 1451 | },{ |
| 1452 | label: "Left-align my posts", |
| 1453 | hint: "Default alignment of your own messages is selected " |
| 1454 | + "based window width/height relationship.", |
| 1455 | boolValue: ()=>!document.body.classList.contains('my-messages-right'), |
| 1456 | callback: function f(){ |
| 1457 | document.body.classList[ |
| 1458 | this.checkbox.checked ? 'remove' : 'add' |
| 1459 | ]('my-messages-right'); |
| 1460 | } |
| 1461 | },{ |
| 1462 | label: "Monospace message font", |
| 1463 | hint: "Use monospace font for message and input text.", |
| 1464 | boolValue: 'monospace-messages', |
| 1465 | callback: function(setting){ |
| 1466 | document.body.classList[ |
| 1467 | setting.value ? 'add' : 'remove' |
| 1468 | ]('monospace-messages'); |
| 1469 | } |
| 1470 | },{ |
| 1471 | label: "Show images inline", |
| 1472 | hint: "Show attached images inline or as a download link.", |
| 1473 | boolValue: 'images-inline' |
| 1474 | }, |
| 1475 | namedOptions.activeUsers, |
| 1476 | { |
| 1477 | label: "Timestamps in active users list", |
| 1478 | hint: "Show most recent message timestamps in the active user list.", |
| 1479 | boolValue: 'active-user-list-timestamps' |
| 1480 | },{ |
| 1481 | label: "Use 'contenteditable' editing mode.", |
| 1482 | boolValue: 'edit-widget-x', |
| 1483 | hint: [ |
| 1484 | "When enabled, chat input uses a so-called 'contenteditable' ", |
| 1485 | "field. Though generally more comfortable and modern than ", |
| 1486 | "plain-text input fields, browser-specific quirks and bugs ", |
| 1487 | "may lead to frustration." |
| 1488 | ].join('') |
| 1489 | }]; |
| 1490 | |
| 1491 | /** Set up selection list of notification sounds. */ |
| 1492 | if(1){ |
| 1493 | const selectSound = D.select(); |
| @@ -1473,11 +1506,14 @@ | |
| 1506 | selectSound.selectedIndex = firstSoundIndex; |
| 1507 | } |
| 1508 | } |
| 1509 | Chat.setNewMessageSound(selectSound.value); |
| 1510 | settingsOps.push({ |
| 1511 | label: "Sound Options...", |
| 1512 | hint: "How to enable audio playback is browser-specific!" |
| 1513 | },{ |
| 1514 | hint: "Audio alert", |
| 1515 | select: selectSound, |
| 1516 | callback: function(ev){ |
| 1517 | const v = ev.target.value; |
| 1518 | Chat.setNewMessageSound(v); |
| 1519 | F.toast.message("Audio notifications "+(v ? "enabled" : "disabled")+"."); |
| @@ -1499,19 +1535,20 @@ | |
| 1535 | const line = D.addClass(D.div(), 'menu-entry'); |
| 1536 | const label = op.label |
| 1537 | ? D.append(D.label(),op.label) : undefined; |
| 1538 | const labelWrapper = D.addClass(D.div(), 'label-wrapper'); |
| 1539 | var hint; |
| 1540 | if(op.hint){ |
| 1541 | hint = D.append(D.addClass(D.span(),'hint'),op.hint); |
| 1542 | } |
| 1543 | if(op.hasOwnProperty('select')){ |
| 1544 | const col0 = D.addClass(D.span(/*empty, but for spacing*/), |
| 1545 | 'toggle-wrapper'); |
| 1546 | D.append(line, labelWrapper, col0); |
| 1547 | D.append(labelWrapper, op.select); |
| 1548 | if(hint) D.append(labelWrapper, hint); |
| 1549 | if(label) D.append(label); |
| 1550 | if(op.callback){ |
| 1551 | op.select.addEventListener('change', (ev)=>op.callback(ev), false); |
| 1552 | } |
| 1553 | }else if(op.hasOwnProperty('boolValue')){ |
| 1554 | if(undefined === f.$id) f.$id = 0; |
| @@ -1523,23 +1560,26 @@ | |
| 1560 | } |
| 1561 | const check = op.checkbox |
| 1562 | = D.attr(D.checkbox(1, op.boolValue()), |
| 1563 | 'aria-label', op.label); |
| 1564 | const id = 'cfgopt'+f.$id; |
| 1565 | const col0 = D.addClass(D.span(), 'toggle-wrapper'); |
| 1566 | check.checked = op.boolValue(); |
| 1567 | op.checkbox = check; |
| 1568 | D.attr(check, 'id', id); |
| 1569 | D.append(line, labelWrapper, col0); |
| 1570 | D.append(col0, check); |
| 1571 | if(label){ |
| 1572 | D.attr(label, 'for', id); |
| 1573 | D.append(labelWrapper, label); |
| 1574 | } |
| 1575 | if(hint) D.append(labelWrapper, hint); |
| 1576 | }else{ |
| 1577 | if(op.callback){ |
| 1578 | line.addEventListener('click', (ev)=>op.callback(ev)); |
| 1579 | } |
| 1580 | D.append(line, labelWrapper); |
| 1581 | if(label) D.append(labelWrapper, label); |
| 1582 | if(hint) D.append(labelWrapper, hint); |
| 1583 | } |
| 1584 | D.append(optionsMenu, line); |
| 1585 | if(op.persistentSetting){ |
| @@ -1577,28 +1617,50 @@ | |
| 1617 | Chat.settings.addListener('active-user-list-timestamps',function(s){ |
| 1618 | Chat.showActiveUserTimestamps(s.value); |
| 1619 | }); |
| 1620 | Chat.settings.addListener('chat-only-mode',function(s){ |
| 1621 | Chat.chatOnlyMode(s.value); |
| 1622 | }); |
| 1623 | Chat.settings.addListener('edit-widget-x',function(s){ |
| 1624 | let eSelected; |
| 1625 | if(s.value){ |
| 1626 | if(Chat.e.inputX===Chat.inputElement()) return; |
| 1627 | eSelected = Chat.e.inputX; |
| 1628 | }else{ |
| 1629 | eSelected = Chat.settings.getBool('edit-compact-mode') |
| 1630 | ? Chat.e.input1 : Chat.e.inputM; |
| 1631 | } |
| 1632 | const v = Chat.inputValue(); |
| 1633 | Chat.inputValue(''); |
| 1634 | Chat.e.inputFields.forEach(function(e,ndx){ |
| 1635 | if(eSelected===e){ |
| 1636 | Chat.e.inputFields.$currentIndex = ndx; |
| 1637 | D.removeClass(e, 'hidden'); |
| 1638 | } |
| 1639 | else D.addClass(e,'hidden'); |
| 1640 | }); |
| 1641 | Chat.inputValue(v); |
| 1642 | eSelected.focus(); |
| 1643 | }); |
| 1644 | Chat.settings.addListener('edit-compact-mode',function(s){ |
| 1645 | if(Chat.e.inputX!==Chat.inputElement()){ |
| 1646 | /* Text field/textarea mode: swap them if needed. |
| 1647 | Compact mode of inputX is toggled via CSS. */ |
| 1648 | const a = s.value |
| 1649 | ? [Chat.e.input1, Chat.e.inputM, 0] |
| 1650 | : [Chat.e.inputM, Chat.e.input1, 1]; |
| 1651 | const v = Chat.inputValue(); |
| 1652 | Chat.inputValue(''); |
| 1653 | Chat.e.inputFields.$currentIndex = a[2]; |
| 1654 | Chat.inputValue(v); |
| 1655 | D.removeClass(a[0], 'hidden'); |
| 1656 | D.addClass(a[1], 'hidden'); |
| 1657 | } |
| 1658 | Chat.e.inputElementWrapper.classList[ |
| 1659 | s.value ? 'add' : 'remove' |
| 1660 | ]('compact'); |
| 1661 | Chat.e.inputFields[Chat.e.inputFields.$currentIndex].focus(); |
| 1662 | }); |
| 1663 | Chat.settings.addListener('edit-ctrl-send',function(s){ |
| 1664 | const label = (s.value ? "Ctrl-" : "")+"Enter submits messages."; |
| 1665 | Chat.e.inputFields.forEach((e)=>{ |
| 1666 | const v = e.dataset.placeholder0 + " " +label; |
| 1667 |
+45
-17
| --- src/style.chat.css | ||
| +++ src/style.chat.css | ||
| @@ -178,18 +178,24 @@ | ||
| 178 | 178 | body.chat:not(.chat-only-mode) #chat-input-area{ |
| 179 | 179 | /* Safari user reports that 2em is necessary to keep the file selection |
| 180 | 180 | widget from overlapping the page footer, whereas a margin of 0 is fine |
| 181 | 181 | for FF/Chrome (and 2em is a *huge* waste of space for those). */ |
| 182 | 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; | |
| 183 | 192 | } |
| 184 | 193 | #chat-input-field-x { |
| 185 | 194 | display: inline-block/*supposed workaround for Chrome weirdness*/; |
| 186 | 195 | padding: 0.2em; |
| 187 | - flex: 10 1 auto; | |
| 188 | 196 | background-color: rgba(156,156,156,0.3); |
| 189 | - overflow: auto; | |
| 190 | - resize: vertical; | |
| 191 | 197 | white-space: pre-wrap; |
| 192 | 198 | /* ^^^ Firefox, when pasting plain text into a contenteditable field, |
| 193 | 199 | loses all newlines unless we explicitly set this. Chrome does not. */ |
| 194 | 200 | cursor: text; |
| 195 | 201 | /* ^^^ In some browsers the cursor may not change for a contenteditable |
| @@ -395,37 +401,59 @@ | ||
| 395 | 401 | /* /chat config options go here */ |
| 396 | 402 | flex: 1 1 auto; |
| 397 | 403 | display: flex; |
| 398 | 404 | flex-direction: column; |
| 399 | 405 | overflow: auto; |
| 406 | + align-items: stretch; | |
| 400 | 407 | } |
| 401 | 408 | body.chat #chat-config #chat-config-options .menu-entry { |
| 402 | 409 | display: flex; |
| 403 | - align-items: baseline; | |
| 410 | + align-items: center; | |
| 404 | 411 | flex-direction: row; |
| 405 | 412 | flex-wrap: nowrap; |
| 406 | 413 | padding: 1em; |
| 414 | + flex: 1 1 auto; | |
| 415 | + align-self: stretch; | |
| 416 | +} | |
| 417 | +body.chat #chat-config #chat-config-options .menu-entry:nth-of-type(even){ | |
| 418 | + background-color: rgba(175,175,175,0.1); | |
| 419 | +} | |
| 420 | +body.chat #chat-config #chat-config-options .menu-entry:nth-of-type(odd){ | |
| 421 | + background-color: rgba(175,175,175,0.25); | |
| 422 | +} | |
| 423 | +body.chat #chat-config #chat-config-options .menu-entry:first-child { | |
| 424 | + /* Config list header */ | |
| 425 | +} | |
| 426 | +body.chat #chat-config #chat-config-options .menu-entry:first-child .label-wrapper { | |
| 427 | + align-items: start; | |
| 428 | +} | |
| 429 | +body.chat #chat-config #chat-config-options .menu-entry > .toggle-wrapper { | |
| 430 | + /* Holder for a checkbox, if any */ | |
| 431 | + min-width: 1.5rem; | |
| 432 | + margin-left: 1rem; | |
| 433 | +} | |
| 434 | +body.chat #chat-config #chat-config-options .menu-entry .label-wrapper { | |
| 435 | + /* Wrapper for a LABEL and a .hint element. */ | |
| 436 | + display: flex; | |
| 437 | + flex-direction: column; | |
| 438 | + align-self: baseline; | |
| 439 | + flex: 1 1 auto; | |
| 440 | +} | |
| 441 | +body.chat #chat-config #chat-config-options .menu-entry label { | |
| 442 | + /* Config option label. */ | |
| 443 | + font-weight: bold; | |
| 444 | + white-space: initial; | |
| 407 | 445 | } |
| 408 | 446 | body.chat #chat-config #chat-config-options .menu-entry label[for] { |
| 409 | 447 | cursor: pointer; |
| 410 | 448 | } |
| 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 { | |
| 449 | +body.chat #chat-config #chat-config-options .menu-entry .hint { | |
| 415 | 450 | /* Config menu hint text */ |
| 416 | - font-size: 80%; | |
| 451 | + font-size: 85%; | |
| 417 | 452 | white-space: pre-wrap; |
| 418 | 453 | 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; | |
| 454 | + opacity: 0.85; | |
| 427 | 455 | } |
| 428 | 456 | body.chat #chat-config #chat-config-options .menu-entry select { |
| 429 | 457 | } |
| 430 | 458 | body.chat #chat-preview #chat-preview-content { |
| 431 | 459 | overflow: auto; |
| 432 | 460 |
| --- src/style.chat.css | |
| +++ src/style.chat.css | |
| @@ -178,18 +178,24 @@ | |
| 178 | body.chat:not(.chat-only-mode) #chat-input-area{ |
| 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-x { |
| 185 | display: inline-block/*supposed workaround for Chrome weirdness*/; |
| 186 | padding: 0.2em; |
| 187 | flex: 10 1 auto; |
| 188 | background-color: rgba(156,156,156,0.3); |
| 189 | overflow: auto; |
| 190 | resize: vertical; |
| 191 | white-space: pre-wrap; |
| 192 | /* ^^^ Firefox, when pasting plain text into a contenteditable field, |
| 193 | loses all newlines unless we explicitly set this. Chrome does not. */ |
| 194 | cursor: text; |
| 195 | /* ^^^ In some browsers the cursor may not change for a contenteditable |
| @@ -395,37 +401,59 @@ | |
| 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 | |
| @@ -178,18 +178,24 @@ | |
| 178 | body.chat:not(.chat-only-mode) #chat-input-area{ |
| 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 |
| @@ -395,37 +401,59 @@ | |
| 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:nth-of-type(even){ |
| 418 | background-color: rgba(175,175,175,0.1); |
| 419 | } |
| 420 | body.chat #chat-config #chat-config-options .menu-entry:nth-of-type(odd){ |
| 421 | background-color: rgba(175,175,175,0.25); |
| 422 | } |
| 423 | body.chat #chat-config #chat-config-options .menu-entry:first-child { |
| 424 | /* Config list header */ |
| 425 | } |
| 426 | body.chat #chat-config #chat-config-options .menu-entry:first-child .label-wrapper { |
| 427 | align-items: start; |
| 428 | } |
| 429 | body.chat #chat-config #chat-config-options .menu-entry > .toggle-wrapper { |
| 430 | /* Holder for a checkbox, if any */ |
| 431 | min-width: 1.5rem; |
| 432 | margin-left: 1rem; |
| 433 | } |
| 434 | body.chat #chat-config #chat-config-options .menu-entry .label-wrapper { |
| 435 | /* Wrapper for a LABEL and a .hint element. */ |
| 436 | display: flex; |
| 437 | flex-direction: column; |
| 438 | align-self: baseline; |
| 439 | flex: 1 1 auto; |
| 440 | } |
| 441 | body.chat #chat-config #chat-config-options .menu-entry label { |
| 442 | /* Config option label. */ |
| 443 | font-weight: bold; |
| 444 | white-space: initial; |
| 445 | } |
| 446 | body.chat #chat-config #chat-config-options .menu-entry label[for] { |
| 447 | cursor: pointer; |
| 448 | } |
| 449 | body.chat #chat-config #chat-config-options .menu-entry .hint { |
| 450 | /* Config menu hint text */ |
| 451 | font-size: 85%; |
| 452 | white-space: pre-wrap; |
| 453 | display: inline-block; |
| 454 | opacity: 0.85; |
| 455 | } |
| 456 | body.chat #chat-config #chat-config-options .menu-entry select { |
| 457 | } |
| 458 | body.chat #chat-preview #chat-preview-content { |
| 459 | overflow: auto; |
| 460 |