Fossil

Check-in [cffd66ffe9]
Login

Check-in [cffd66ffe9]

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Initial go at a "bottom-up" (mobile-like) layout for chat, but it is only active in chat-only mode where we have more control over the layout. The default mode works like before, top-down. There are still minor usability/scrolling issues left to resolve but it seems to essentially work.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | chat-mode-bottom-up
Files: files | file ages | folders
SHA3-256: cffd66ffe9ed970d831383df282397ace7b61addade191e076ae4eb098658893
User & Date: stephan 2020-12-26 01:31:05.977
Context
2020-12-26
01:37
Settings menu now closes if a click or ESC happens outside of the menu. ... (check-in: 1f00036884 user: stephan tags: chat-mode-bottom-up)
01:31
Initial go at a "bottom-up" (mobile-like) layout for chat, but it is only active in chat-only mode where we have more control over the layout. The default mode works like before, top-down. There are still minor usability/scrolling issues left to resolve but it seems to essentially work. ... (check-in: cffd66ffe9 user: stephan tags: chat-mode-bottom-up)
2020-12-25
23:38
CSS docs and line-wrapped the new settings icon data URL. No functional changes. ... (check-in: ca42098af0 user: stephan tags: trunk)
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/chat.js.
1
2
3
4
5
6
7
8
9
10
11

12
13
14
15
16
17
18
/**
   This file contains the client-side implementation of fossil's /chat
   application. 
*/
(function(){
  const F = window.fossil, D = F.dom;
  const E1 = function(selector){
    const e = document.querySelector(selector);
    if(!e) throw new Error("missing required DOM element: "+selector);
    return e;
  };

  const Chat = (function(){
    const cs = {
      e:{/*map of certain DOM elements.*/
        messageInjectPoint: E1('#message-inject-point'),
        pageTitle: E1('head title'),
        loadToolbar: undefined /* the load-posts toolbar (dynamically created) */,
        inputWrapper: E1("#chat-input-area"),











>







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
   This file contains the client-side implementation of fossil's /chat
   application. 
*/
(function(){
  const F = window.fossil, D = F.dom;
  const E1 = function(selector){
    const e = document.querySelector(selector);
    if(!e) throw new Error("missing required DOM element: "+selector);
    return e;
  };
  //document.body.classList.add('chat-only-mode');
  const Chat = (function(){
    const cs = {
      e:{/*map of certain DOM elements.*/
        messageInjectPoint: E1('#message-inject-point'),
        pageTitle: E1('head title'),
        loadToolbar: undefined /* the load-posts toolbar (dynamically created) */,
        inputWrapper: E1("#chat-input-area"),
110
111
112
113
114
115
116
117
118


119





















120
121

122
















123



124
125
126
127
128
129
130
         list if atEnd is falsy, else at the end of the list, before
         the load-history widget. */
      injectMessageElem: function f(e, atEnd){
        const mip = atEnd ? this.e.loadToolbar : this.e.messageInjectPoint;
        if(atEnd){
          mip.parentNode.insertBefore(e, mip);
        }else{
          if(mip.nextSibling){
            mip.parentNode.insertBefore(e, mip.nextSibling);


          }else{





















            mip.parentNode.appendChild(e);
          }

        }
















      },



      settings:{
        get: (k,dflt)=>F.storage.get(k,dflt),
        getBool: (k,dflt)=>F.storage.getBool(k,dflt),
        set: (k,v)=>F.storage.set(k,v),
        defaults:{
          "images-inline": !!F.config.chat.imagesInline,
          "monospace-messages": false







|
|
>
>

>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
|
>

>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>

>
>
>







111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
         list if atEnd is falsy, else at the end of the list, before
         the load-history widget. */
      injectMessageElem: function f(e, atEnd){
        const mip = atEnd ? this.e.loadToolbar : this.e.messageInjectPoint;
        if(atEnd){
          mip.parentNode.insertBefore(e, mip);
        }else{
          if(mip.nextSibling) mip.parentNode.insertBefore(e, mip.nextSibling);
          else mip.parentNode.appendChild(e);
          if(this.isChatOnlyMode()){
            e.scrollIntoView();
          }else{
            //const rect = e.getBoundingClientRect();
            //const rect = this.e.inputWrapper.getBoundingClientRect();
            //window.scrollBy(0, -cs.height);
            //console.debug("rect =",rect);
            //window.scrollBy(0,rect.height);
            //window.scrollTo(0,rect.top);
            //e.querySelector('.message-widget-tab').scrollIntoView();
          }
        }
      },
      isChatOnlyMode: function(){
        return document.body.classList.contains('chat-only-mode');
      },
      chatOnlyMode: function f(yes){
        if(undefined === f.elemsToToggle){
          f.elemsToToggle = [];
          document.body.childNodes.forEach(function(e){
            if(!e.classList) return/*TEXT nodes and such*/;
            else if(!e.classList.contains('content')
                    && !e.classList.contains('fossil-PopupWidget')
                    /*kludge^^^ for settingsPopup click handling!*/){
              f.elemsToToggle.push(e);
            }
          });
        }
        if(!arguments.length) yes = true;
        if(yes === this.isChatOnlyMode()) return this;
        if(yes){
          D.addClass(f.elemsToToggle, 'hidden');
          D.addClass(document.body, 'chat-only-mode');
          document.body.scroll(0,document.body.height);
        }else{
          D.removeClass(f.elemsToToggle, 'hidden');
          D.removeClass(document.body, 'chat-only-mode');
          setTimeout(()=>document.body.scrollIntoView(
            /*moves to (0,0), whereas scrollTo(0,0) does not!*/
          ), 0);
        }
        const msg = document.querySelector('.message-widget');
        if(msg) msg.scrollIntoView();
        return this;
      },
      toggleChatOnlyMode: function(){
        return this.chatOnlyMode(!this.isChatOnlyMode());
      },
      settings:{
        get: (k,dflt)=>F.storage.get(k,dflt),
        getBool: (k,dflt)=>F.storage.getBool(k,dflt),
        set: (k,v)=>F.storage.set(k,v),
        defaults:{
          "images-inline": !!F.config.chat.imagesInline,
          "monospace-messages": false
143
144
145
146
147
148
149


















150
151
152
153
154
155
156
      document.body.classList.add('my-messages-right');
    }
    if(cs.settings.getBool('monospace-messages',false)){
      document.body.classList.add('monospace-messages');
    }
    cs.e.inputCurrent = cs.e.inputSingle;
    cs.pageTitleOrig = cs.e.pageTitle.innerText;


















    const qs = (e)=>document.querySelector(e);
    const argsToArray = function(args){
      return Array.prototype.slice.call(args,0);
    };
    cs.reportError = function(/*msg args*/){
      const args = argsToArray(arguments);
      console.error("chat error:",args);







>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>







187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
      document.body.classList.add('my-messages-right');
    }
    if(cs.settings.getBool('monospace-messages',false)){
      document.body.classList.add('monospace-messages');
    }
    cs.e.inputCurrent = cs.e.inputSingle;
    cs.pageTitleOrig = cs.e.pageTitle.innerText;

    if(true){
      /* In order to make the input area opaque, such that the message
         list scrolls under it without being visible, we have to
         ensure that the input area has a non-transparent background
         color. Ideally we'd select the color of div.content, but that
         is not necessarily set, so we fall back to using the body's
         background color and hope it's been explicitly set
         somewhere. If we rely on the input area having its own color
         specified in CSS then all skins would have to define an
         appropriate color. Thus our selection of the body color,
         while slightly unfortunate, is in the interest of keeping
         skins from being forced to define an opaque bg color.
      */
      const bodyStyle = window.getComputedStyle(document.body);
      cs.e.inputWrapper.style.backgroundColor = bodyStyle.backgroundColor;
    }

    const qs = (e)=>document.querySelector(e);
    const argsToArray = function(args){
      return Array.prototype.slice.call(args,0);
    };
    cs.reportError = function(/*msg args*/){
      const args = argsToArray(arguments);
      console.error("chat error:",args);
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
    f.popup.show(x, y);
  }/*handleLegendClicked()*/;

  (function(){/*Set up #chat-settings-button */
    const settingsButton = document.querySelector('#chat-settings-button');
    var popupSize = undefined/*placement workaround*/;
    const settingsPopup = new F.PopupWidget({
      cssClass: ['fossil-tooltip', 'chat-settings-popup'],
      adjustY: function(y){
        const rect = settingsButton.getBoundingClientRect();
        return rect.top + rect.height + 2;
      }
    });
    /* Settings menu entries... */
    const settingsOps = [{
      label: "Multi-line input",
      boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti,
      callback: function(){
        Chat.inputToggleSingleMulti();
      }
    },{
      label: "Monospace message font",
      boolValue: ()=>document.body.classList.contains('monospace-messages'),
      callback: function(){
        document.body.classList.toggle('monospace-messages');
        Chat.settings.set('monospace-messages',
                          document.body.classList.contains('monospace-messages'));
      }
    },{
      label: "Chat-only mode",
      boolValue: ()=>!!document.body.classList.contains('chat-only-mode'),
      callback: function f(){
        if(undefined === f.isHidden){
          f.isHidden = false;
          f.elemsToToggle = [];
          document.body.childNodes.forEach(function(e){
            if(!e.classList) return/*TEXT nodes and such*/;
            else if(!e.classList.contains('content')
                    && !e.classList.contains('fossil-PopupWidget')
                    /*kludge^^^ for settingsPopup click handling!*/){
              f.elemsToToggle.push(e);
            }
          });
          /* In order to make the input area opaque, such that the
             message list scrolls under it without being visible, we
             have to ensure that the input area has a non-inherited
             background color. Ideally we'd select the color of
             div.content, but that is not necessarily set, so we fall
             back to using the body's background color. If we rely on
             the input area having its own color specified in CSS then
             all skins would have to define an appropriate color.
             Thus our selection of the body color, while slightly unfortunate,
             is in the interest of keeping skins from being forced to
             define an opaque bg color.
          */
          f.initialBg = Chat.e.messagesWrapper.style.backgroundColor;
          const cs = window.getComputedStyle(document.body);
          f.inheritedBg = cs.backgroundColor;
        }
        const iws = Chat.e.inputWrapper.style;
        if((f.isHidden = !f.isHidden)){
          D.addClass(f.elemsToToggle, 'hidden');
          D.addClass(document.body, 'chat-only-mode');
          iws.backgroundColor = f.inheritedBg;
        }else{
          D.removeClass(f.elemsToToggle, 'hidden');
          D.removeClass(document.body, 'chat-only-mode');
          iws.backgroundColor = f.initialBg;
        }
      }
    },{
      label: "Left-align my posts",
      boolValue: ()=>!document.body.classList.contains('my-messages-right'),
      callback: function f(){
        document.body.classList.toggle('my-messages-right');
      }







|
<
<
<
<


















|
|
<
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<







577
578
579
580
581
582
583
584




585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604


605


































606
607
608
609
610
611
612
    f.popup.show(x, y);
  }/*handleLegendClicked()*/;

  (function(){/*Set up #chat-settings-button */
    const settingsButton = document.querySelector('#chat-settings-button');
    var popupSize = undefined/*placement workaround*/;
    const settingsPopup = new F.PopupWidget({
      cssClass: ['fossil-tooltip', 'chat-settings-popup']




    });
    /* Settings menu entries... */
    const settingsOps = [{
      label: "Multi-line input",
      boolValue: ()=>Chat.inputElement()===Chat.e.inputMulti,
      callback: function(){
        Chat.inputToggleSingleMulti();
      }
    },{
      label: "Monospace message font",
      boolValue: ()=>document.body.classList.contains('monospace-messages'),
      callback: function(){
        document.body.classList.toggle('monospace-messages');
        Chat.settings.set('monospace-messages',
                          document.body.classList.contains('monospace-messages'));
      }
    },{
      label: "Chat-only mode",
      boolValue: ()=>Chat.isChatOnlyMode(),
      callback: function(){


        Chat.toggleChatOnlyMode();


































      }
    },{
      label: "Left-align my posts",
      boolValue: ()=>!document.body.classList.contains('my-messages-right'),
      callback: function f(){
        document.body.classList.toggle('my-messages-right');
      }
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647








648
649
650
651
652
653
654
      //ev.preventDefault();
      if(settingsPopup.isShown()) settingsPopup.hide();
      else settingsPopup.show(settingsButton);
      /* Reminder: we cannot toggle the visibility from her
       */
    }, false);

    /* Find an ideal X position for the popup, directly under the settings
       button, based on the size of the popup... */
    settingsPopup.show(document.body);
    popupSize = settingsPopup.e.getBoundingClientRect();
    settingsPopup.hide();
    settingsPopup.options.adjustX = function(x){
      const rect = settingsButton.getBoundingClientRect();
      return rect.right - popupSize.width;








    };
  })()/*#chat-settings-button setup*/;

  
  /** Callback for poll() to inject new content into the page.  jx ==
      the response from /chat-poll. If atEnd is true, the message is
      appended to the end of the chat list, else the beginning (the







|







>
>
>
>
>
>
>
>







655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
      //ev.preventDefault();
      if(settingsPopup.isShown()) settingsPopup.hide();
      else settingsPopup.show(settingsButton);
      /* Reminder: we cannot toggle the visibility from her
       */
    }, false);

    /* Find an ideal X/Y position for the popup, directly above the settings
       button, based on the size of the popup... */
    settingsPopup.show(document.body);
    popupSize = settingsPopup.e.getBoundingClientRect();
    settingsPopup.hide();
    settingsPopup.options.adjustX = function(x){
      const rect = settingsButton.getBoundingClientRect();
      return rect.right - popupSize.width;
    };
    settingsPopup.options.adjustY = function(y){
      const rect = settingsButton.getBoundingClientRect();
      if(Chat.isChatOnlyMode()){
        return rect.top - popupSize.height -2;
      }else{
        return rect.bottom + 2;
      }
    };
  })()/*#chat-settings-button setup*/;

  
  /** Callback for poll() to inject new content into the page.  jx ==
      the response from /chat-poll. If atEnd is true, the message is
      appended to the end of the chat list, else the beginning (the
Changes to src/default.css.
1595
1596
1597
1598
1599
1600
1601




1602
1603
1604
1605
1606
1607
1608
  border-radius: 0.25em;
}
.settings-icon:hover {
  border: 1px outset rgba(127,127,127,1);
}
body.fossil-dark-style .settings-icon {
  filter: invert(100%);




}
body.chat #chat-settings-button {
}
/** Popup widget for the /chat settings. */
body.chat .chat-settings-popup {
  font-size: 0.8em;
  text-align: left;







>
>
>
>







1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
  border-radius: 0.25em;
}
.settings-icon:hover {
  border: 1px outset rgba(127,127,127,1);
}
body.fossil-dark-style .settings-icon {
  filter: invert(100%);
}
/* "Chat-only mode" hides the site header/footer, showing only
   the chat app. */
body.chat.chat-only-mode{
}
body.chat #chat-settings-button {
}
/** Popup widget for the /chat settings. */
body.chat .chat-settings-popup {
  font-size: 0.8em;
  text-align: left;
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647






1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681

1682














1683
1684
1685
1686
1687
1688
1689
  cursor: inherit;
}
/** Container for the list of /chat messages. */
body.chat #chat-messages-wrapper {
  display: flex;
  flex-direction: column;
}
/* "Chat-only mode" hides the site header/footer, showing only
   the chat app. */
body.chat.chat-only-mode{






}
body.chat.chat-only-mode > div.content {
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  align-items: stretch;
}
body.chat.chat-only-mode #chat-input-area {
  /* would like to pin this to the top so that it stays in place when
 scrolling, but doing so causes #chat-messages-wrapper to scroll
 behind it visibly, which is really ugly. Only current workaround is
 to force an opaque background color on this element, but that's not
 skin-friendly. */
  position: sticky;
  position: -webkit-sticky /* supposedly some versions of Safari */;
  top: 0;
  padding: 0.5em 1em;
  z-index: 100
    /* see notes in #chat-messages-wrapper. The various popups require a
       z-index higher than this one. */
}
body.chat.chat-only-mode #chat-messages-wrapper {
  position: relative;
  top: 0;
  z-index: 99 /* so that it scrolls under input area. If it's
                 lower than div.content then mouse events to it
                 are blocked!*/;
}
/* Wrapper for /chat user input controls */
body.chat #chat-input-area {
  display: flex;
  flex-direction: column;
  border-bottom: 1px solid black;

  margin-bottom: 0.5em;














}
body.chat #chat-input-line {
  display: flex;
  flex-direction: row;
  margin-bottom: 0.25em;
  align-items: flex-start;
}







<
<
|
>
>
>
>
>
>








|
<
<
<
<
<
|
<
<
<
<
<
<
<
<
<
<
<
<
<






>

>
>
>
>
>
>
>
>
>
>
>
>
>
>







1642
1643
1644
1645
1646
1647
1648


1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664





1665













1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
  cursor: inherit;
}
/** Container for the list of /chat messages. */
body.chat #chat-messages-wrapper {
  display: flex;
  flex-direction: column;
}


body.chat.chat-only-mode #chat-messages-wrapper {
  flex-direction: column-reverse;
  position: relative;
  top: 0;
  z-index: 99 /* so that it scrolls under input area. If it's
                 lower than div.content then mouse events to it
                 are blocked!*/;
}
body.chat.chat-only-mode > div.content {
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  align-items: stretch;
}
body.chat.chat-only-mode > div.content {





  flex-direction: column-reverse;













}
/* Wrapper for /chat user input controls */
body.chat #chat-input-area {
  display: flex;
  flex-direction: column;
  border-bottom: 1px solid black;
  padding: 0.5em 1em;
  margin-bottom: 0.5em;
  /*position: sticky; top: 0;*/
  /*position: -webkit-sticky*/ /* supposedly some versions of Safari */;
}
body.chat.chat-only-mode #chat-input-area {
  z-index: 100
    /* see notes in #chat-messages-wrapper. The various popups require a
       z-index higher than this one. */;
  border-bottom: none;
  border-top: 1px solid black;
  margin-bottom: 0;
  margin-top: 0.5em;
  position: sticky;
  position: -webkit-sticky/* supposedly some versions of Safari */;
  bottom: 0;
}
body.chat #chat-input-line {
  display: flex;
  flex-direction: row;
  margin-bottom: 0.25em;
  align-items: flex-start;
}