Fossil

Check-in [9085ec2351]
Login

Check-in [9085ec2351]

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

Overview
Comment:Minor code style improvements, fixed a couple jquery-isms in the confirmer port, added the ability to select tabs by number and pre-select a specific tab, removed a superfluous C function.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | fileedit-ajaxify
Files: files | file ages | folders
SHA3-256: 9085ec2351d42966153cb6b13ed15f0c4a20611e4fd901694898857b8c696496
User & Date: stephan 2020-05-06 04:52:29.446
Context
2020-05-06
05:21
cosmetic internal tweaks. ... (check-in: bb71f9ecd2 user: stephan tags: fileedit-ajaxify)
04:52
Minor code style improvements, fixed a couple jquery-isms in the confirmer port, added the ability to select tabs by number and pre-select a specific tab, removed a superfluous C function. ... (check-in: 9085ec2351 user: stephan tags: fileedit-ajaxify)
03:30
Removed a bogus, but harmless, JS dep and corrected some copy/pasted docs. ... (check-in: 2abc1ff42a user: stephan tags: fileedit-ajaxify)
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/default_css.txt.
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
////////////////////////////////////////////////////////////////////
// Styles developed for /fileedit but which have wider
// applicability:
.flex-container {
    display: flex;
}
.flex-container.row {
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
}
.flex-container.row.stretch {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: stretch;
    margin: 0;
}
.flex-container.column {
    flex-direction: column;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
}
.flex-container.column.stretch {
    align-items: stretch;
    margin: 0;
}
.font-size-100 {
  font-size: 100%;
}
.font-size-125 {
  font-size: 125%;
}







|
|
|
|


|
|
|
|


|
|
|
|


|
|







1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
////////////////////////////////////////////////////////////////////
// Styles developed for /fileedit but which have wider
// applicability:
.flex-container {
    display: flex;
}
.flex-container.row {
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
}
.flex-container.row.stretch {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: stretch;
  margin: 0;
}
.flex-container.column {
  flex-direction: column;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
}
.flex-container.column.stretch {
  align-items: stretch;
  margin: 0;
}
.font-size-100 {
  font-size: 100%;
}
.font-size-125 {
  font-size: 125%;
}
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
  display: inline-block;
  cursor: pointer;
}
.input-with-label > * {
  vertical-align: middle;
}
.input-with-label > input {
    margin: 0;
}
.input-with-label > select {
  margin: 0;
}
.input-with-label > input[type=text] {
  margin: 0;
}
.input-with-label > textarea {
  margin: 0;
}
.input-with-label > input[type=checkbox] {
    vertical-align: sub;
}
.input-with-label > input[type=radio] {
    vertical-align: sub;
}
.input-with-label > span {
    margin: 0 0.25em 0 0.25em;
    vertical-align: middle;
}







|











|


|


|
|

1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
  display: inline-block;
  cursor: pointer;
}
.input-with-label > * {
  vertical-align: middle;
}
.input-with-label > input {
  margin: 0;
}
.input-with-label > select {
  margin: 0;
}
.input-with-label > input[type=text] {
  margin: 0;
}
.input-with-label > textarea {
  margin: 0;
}
.input-with-label > input[type=checkbox] {
  vertical-align: sub;
}
.input-with-label > input[type=radio] {
  vertical-align: sub;
}
.input-with-label > span {
  margin: 0 0.25em 0 0.25em;
  vertical-align: middle;
}
Changes to src/fileedit.c.
26
27
28
29
30
31
32
33
34

35
36
37
38
39
40
41
** State for the "mini-checkin" infrastructure, which enables the
** ability to commit changes to a single file without a checkout
** db, e.g. for use via an HTTP request.
**
** Use CheckinMiniInfo_init() to cleanly initialize one to a known
** valid/empty default state.
**
** Memory for all non-const (char *) members is owned by the
** CheckinMiniInfo instance and is freed by CheckinMiniInfo_cleanup().

*/
struct CheckinMiniInfo {
  Manifest * pParent;  /* parent checkin. Memory is owned by this
                          object. */
  char *zParentUuid;   /* Full UUID of pParent */
  char *zFilename;     /* Name of single file to commit. Must be
                          relative to the top of the repo. */







|

>







26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
** State for the "mini-checkin" infrastructure, which enables the
** ability to commit changes to a single file without a checkout
** db, e.g. for use via an HTTP request.
**
** Use CheckinMiniInfo_init() to cleanly initialize one to a known
** valid/empty default state.
**
** Memory for all non-const pointer members is owned by the
** CheckinMiniInfo instance and is freed by CheckinMiniInfo_cleanup().
** Similarly, each instance owns any memory for its Blob members.
*/
struct CheckinMiniInfo {
  Manifest * pParent;  /* parent checkin. Memory is owned by this
                          object. */
  char *zParentUuid;   /* Full UUID of pParent */
  char *zFilename;     /* Name of single file to commit. Must be
                          relative to the top of the repo. */
166
167
168
169
170
171
172
173

174
175
176
177
178
179
180
181
182
183
184
185
186
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
  fossil_free(p->zCommentMimetype);
  fossil_free(p->zUser);
  CheckinMiniInfo_init(p);
}

/*
** Internal helper which returns an F-card perms string suitable for
** writing into a manifest.

*/
static const char * mfile_permint_mstring(int perm){
  switch(perm){
    case PERM_EXE: return " x";
    case PERM_LNK: return " l";
    default: return "";
  }
}

/*
** Given a ManifestFile permission string (or NULL), it returns one of
** PERM_REG, PERM_EXE, or PERM_LNK.
*/
static int mfile_permstr_int(const char *zPerm){
  if(!zPerm || !*zPerm) return PERM_REG;
  else if(strstr(zPerm,"x")) return PERM_EXE;
  else if(strstr(zPerm,"l")) return PERM_LNK;
  else return PERM_REG/*???*/;
}

static const char * mfile_perm_mstring(const ManifestFile * p){
  return mfile_permint_mstring(manifest_file_mperm(p));
}

/*
** Internal helper for checkin_mini() and friends. Appends an F-card
** for p to pOut.
*/
static void checkin_mini_append_fcard(Blob *pOut, const ManifestFile *p){

  if(p->zUuid){
    assert(*p->zUuid);
    blob_appendf(pOut, "F %F %s%s", p->zName,
                 p->zUuid, mfile_perm_mstring(p));

    if(p->zPrior){
      assert(*p->zPrior);
      blob_appendf(pOut, " %F\n", p->zPrior);
    }else{
      blob_append(pOut, "\n", 1);
    }
  }else{







|
>




















<
<
<
<




|
>



|
>







167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
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
  fossil_free(p->zCommentMimetype);
  fossil_free(p->zUser);
  CheckinMiniInfo_init(p);
}

/*
** Internal helper which returns an F-card perms string suitable for
** writing as-is into a manifest. If it's not empty, it includes a
** leading space to separate it from the F-card's hash field.
*/
static const char * mfile_permint_mstring(int perm){
  switch(perm){
    case PERM_EXE: return " x";
    case PERM_LNK: return " l";
    default: return "";
  }
}

/*
** Given a ManifestFile permission string (or NULL), it returns one of
** PERM_REG, PERM_EXE, or PERM_LNK.
*/
static int mfile_permstr_int(const char *zPerm){
  if(!zPerm || !*zPerm) return PERM_REG;
  else if(strstr(zPerm,"x")) return PERM_EXE;
  else if(strstr(zPerm,"l")) return PERM_LNK;
  else return PERM_REG/*???*/;
}





/*
** Internal helper for checkin_mini() and friends. Appends an F-card
** for p to pOut.
*/
static void checkin_mini_append_fcard(Blob *pOut,
                                      const ManifestFile *p){
  if(p->zUuid){
    assert(*p->zUuid);
    blob_appendf(pOut, "F %F %s%s", p->zName,
                 p->zUuid,
                 mfile_permint_mstring(manifest_file_mperm(p)));
    if(p->zPrior){
      assert(*p->zPrior);
      blob_appendf(pOut, " %F\n", p->zPrior);
    }else{
      blob_append(pOut, "\n", 1);
    }
  }else{
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684

1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
       "</div>");
    CX("<div id='fileedit-tab-diff-wrapper'>"
       "Diffs will be shown here."
       "</div>");
    CX("</div>"/*#fileedit-tab-diff*/);
  }


  /****** Commit ******/
  CX("<div id='fileedit-tab-commit' "
     "data-tab-parent='fileedit-tabs' "

     "data-tab-label='Commit'"
     ">");

  {
    /******* Flags/options *******/
    CX("<div class='fileedit-options flex-container row'>");
    style_labeled_checkbox("cb-dry-run",
                           "dry_run", "Dry-run?", "1",
                           "In dry-run mode, the Save button performs "
                           "all work needed for saving but then rolls "
                           "back the transaction, and thus does not "
                           "really save.",







<



>




|







1674
1675
1676
1677
1678
1679
1680

1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
       "</div>");
    CX("<div id='fileedit-tab-diff-wrapper'>"
       "Diffs will be shown here."
       "</div>");
    CX("</div>"/*#fileedit-tab-diff*/);
  }


  /****** Commit ******/
  CX("<div id='fileedit-tab-commit' "
     "data-tab-parent='fileedit-tabs' "
     "data-tab-select='1' "
     "data-tab-label='Commit'"
     ">");

  {
    /******* Commit flags/options *******/
    CX("<div class='fileedit-options flex-container row'>");
    style_labeled_checkbox("cb-dry-run",
                           "dry_run", "Dry-run?", "1",
                           "In dry-run mode, the Save button performs "
                           "all work needed for saving but then rolls "
                           "back the transaction, and thus does not "
                           "really save.",
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
                          "Inherit", 0,
                          "Unix", 1,
                          "Windows", 2,
                          NULL);
    CX("</div>"/*checkboxes*/);
  }

  { /******* Comment *******/
    CX("<fieldset class='fileedit-options'>"
       "<legend>Message (required)</legend><div>");
    CX("<input type='text' name='comment' "
       "id='fileedit-comment'>");
    /* ^^^ adding the 'required' attribute means we cannot even
       submit for PREVIEW mode if it's empty :/. */
    if(blob_size(&cimi.comment)){







|







1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
                          "Inherit", 0,
                          "Unix", 1,
                          "Windows", 2,
                          NULL);
    CX("</div>"/*checkboxes*/);
  }

  { /******* Commit comment, button, and result manifest *******/
    CX("<fieldset class='fileedit-options'>"
       "<legend>Message (required)</legend><div>");
    CX("<input type='text' name='comment' "
       "id='fileedit-comment'>");
    /* ^^^ adding the 'required' attribute means we cannot even
       submit for PREVIEW mode if it's empty :/. */
    if(blob_size(&cimi.comment)){
1780
1781
1782
1783
1784
1785
1786



1787

1788
1789
1790
1791
1792
1793
1794
  style_emit_script_fetch(0);
  style_emit_script_tabs(0);
  style_emit_script_builtin("fossil.page.fileedit.js",0);
  style_emit_script_confirmer(0);
  if(blob_size(&endScript)>0){
    style_emit_script_tag(0,0);
    CX("(function(){\n");



    CX("try{\n%b\n}catch(e){console.error('Exception:',e)}\n",

       &endScript);
    CX("})();");
    style_emit_script_tag(1,0);
  }
  db_end_transaction(0);
  style_footer();
}







>
>
>
|
>







1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
  style_emit_script_fetch(0);
  style_emit_script_tabs(0);
  style_emit_script_builtin("fossil.page.fileedit.js",0);
  style_emit_script_confirmer(0);
  if(blob_size(&endScript)>0){
    style_emit_script_tag(0,0);
    CX("(function(){\n");
    CX("try{\n%b\n}"
       "catch(e){"
       "fossil.error(e);\n"
       "console.error('Exception:',e);\n"
       "}\n",
       &endScript);
    CX("})();");
    style_emit_script_tag(1,0);
  }
  db_end_transaction(0);
  style_footer();
}
Changes to src/fossil.confirmer.js.

1
2
3
4
5
6
7

/**************************************************************
Confirmer is a utility which provides an alternative to confirmation
dialog boxes and "check this checkbox to confirm action" widgets. It
acts by modifying a button to require two clicks within a certain
time, with the second click acting as a confirmation of the first. If
the second click does not come within a specified timeout then the
action is not confirmed.
>







1
2
3
4
5
6
7
8
"use strict";
/**************************************************************
Confirmer is a utility which provides an alternative to confirmation
dialog boxes and "check this checkbox to confirm action" widgets. It
acts by modifying a button to require two clicks within a certain
time, with the second click acting as a confirmation of the first. If
the second click does not come within a specified timeout then the
action is not confirmed.
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
  .classInitial = optional CSS class string (default='') which is
  added to the element during its "initial" state (the state it is in
  when it is not waiting on a timeout). When the target is activated
  (waiting on a timeout) this class is removed.  In the case of a
  timeout, this class is added *before* the .ontimeout handler is
  called.

  .classActivated = optional CSS class string (default='') which is
  added to the target when it is waiting on a timeout. When the target
  leaves timeout-wait mode, this class is removed.  When timeout-wait
  mode is entered, this class is added *before* the .onactivate
  handler is called.

  .debug = boolean. If truthy, it sends some debug output to the dev
  console to track what it's doing.

Due to the nature of multi-threaded code, it is potentially possible
that confirmation and timeout actions BOTH happen if the user triggers
the associated action at "just the right millisecond" before the
timeout is triggered.

To change the default option values, modify the
fossil.confirmer.defaultOpts object.

Terse Change history:

20200506:
  - Ported from jQuery to plain JS.

- 20181112:
  - extended to support certain INPUT elements.
  - made default opts configurable.

- 20070717: initial jQuery-based impl.
*/
(function(F/*the fossil object*/){
  "use strict";

  F.confirmer = function f(elem,opt){

    const dbg = opt.debug
          ? function(){console.debug.apply(console,arguments)}
          : function(){};
    dbg("confirmer opt =",opt);
    if(!f.Holder){
      f.isInput = (e)=>/^(input|textarea)$/i.test(e.nodeName);
      f.Holder = function(target,opt){







|


















|









<
<

<







50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85


86

87
88
89
90
91
92
93
  .classInitial = optional CSS class string (default='') which is
  added to the element during its "initial" state (the state it is in
  when it is not waiting on a timeout). When the target is activated
  (waiting on a timeout) this class is removed.  In the case of a
  timeout, this class is added *before* the .ontimeout handler is
  called.

  .classWaiting = optional CSS class string (default='') which is
  added to the target when it is waiting on a timeout. When the target
  leaves timeout-wait mode, this class is removed.  When timeout-wait
  mode is entered, this class is added *before* the .onactivate
  handler is called.

  .debug = boolean. If truthy, it sends some debug output to the dev
  console to track what it's doing.

Due to the nature of multi-threaded code, it is potentially possible
that confirmation and timeout actions BOTH happen if the user triggers
the associated action at "just the right millisecond" before the
timeout is triggered.

To change the default option values, modify the
fossil.confirmer.defaultOpts object.

Terse Change history:

- 20200506:
  - Ported from jQuery to plain JS.

- 20181112:
  - extended to support certain INPUT elements.
  - made default opts configurable.

- 20070717: initial jQuery-based impl.
*/
(function(F/*the fossil object*/){


  F.confirmer = function f(elem,opt){

    const dbg = opt.debug
          ? function(){console.debug.apply(console,arguments)}
          : function(){};
    dbg("confirmer opt =",opt);
    if(!f.Holder){
      f.isInput = (e)=>/^(input|textarea)$/i.test(e.nodeName);
      f.Holder = function(target,opt){
119
120
121
122
123
124
125
126



127
128
129
130
131
132
133
            this.opt.ontimeout.call(this.target);
          }
        };
        target.addEventListener(
          'click', function(){
            switch( self.state ) {
            case( self.states.waiting ):
              if( undefined !== self.timerID ) clearTimeout( self.timerID );



              self.state = self.states.initial;
              self.setClasses( false );
              dbg("Confirmed");
              updateText(self.opt.initialText);
              if( self.opt.onconfirm ) self.opt.onconfirm.call(self.target);
              break;
            case( self.states.initial ):







|
>
>
>







117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
            this.opt.ontimeout.call(this.target);
          }
        };
        target.addEventListener(
          'click', function(){
            switch( self.state ) {
            case( self.states.waiting ):
              if( undefined !== self.timerID ){
                clearTimeout( self.timerID );
                delete self.timerID;
              }
              self.state = self.states.initial;
              self.setClasses( false );
              dbg("Confirmed");
              updateText(self.opt.initialText);
              if( self.opt.onconfirm ) self.opt.onconfirm.call(self.target);
              break;
            case( self.states.initial ):
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
175
            default: // can't happen.
              break;
            }
          }, false
        );
      };
      f.Holder.prototype = {
        states:{
          initial: 0, waiting: 1
        },
        setClasses: function(activated) {
          if( activated ) {
            if( this.opt.classActivated ) {
              this.target.addClass( this.opt.classActivated );
            }
            if( this.opt.classInitial ) {
              this.target.removeClass( this.opt.classInitial );
            }
          } else {
            if( this.opt.classInitial ) {
              this.target.addClass( this.opt.classInitial );
            }
            if( this.opt.classActivated ) {
              this.target.removeClass( this.opt.classActivated );
            }
          }
        }
        
      };
    }/*static init*/
    opt = F.mergeLastWins(f.defaultOpts,{
      initialText: (
        f.isInput(elem) ? elem.value : elem.innerHTML
      ) || "PLEASE SET .initialText"
    },opt);







<
|
<

|
|
|


|

|

|

|
|



<







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
            default: // can't happen.
              break;
            }
          }, false
        );
      };
      f.Holder.prototype = {

        states:{initial: 0, waiting: 1},

        setClasses: function(activated) {
          if(activated) {
            if( this.opt.classWaiting ) {
              this.target.classList.add( this.opt.classWaiting );
            }
            if( this.opt.classInitial ) {
              this.target.classList.remove( this.opt.classInitial );
            }
          }else{
            if( this.opt.classInitial ) {
              this.target.classList.add( this.opt.classInitial );
            }
            if( this.opt.classWaiting ) {
              this.target.classList.remove( this.opt.classWaiting );
            }
          }
        }

      };
    }/*static init*/
    opt = F.mergeLastWins(f.defaultOpts,{
      initialText: (
        f.isInput(elem) ? elem.value : elem.innerHTML
      ) || "PLEASE SET .initialText"
    },opt);
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
     The default options for initConfirmer(). Tweak them to set the
     defaults. A couple of them (initialText and confirmText) are
     dynamically-generated, and can't reasonably be set in the
     defaults.
  */
  F.confirmer.defaultOpts = {
    timeout:3000,
    onconfirm:undefined,
    ontimeout:undefined,
    onactivate:undefined,
    classInitial:'',
    classActivated:'',
    debug:true
  };

})(window.fossil);







|
|
|
|
|
|



181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
     The default options for initConfirmer(). Tweak them to set the
     defaults. A couple of them (initialText and confirmText) are
     dynamically-generated, and can't reasonably be set in the
     defaults.
  */
  F.confirmer.defaultOpts = {
    timeout:3000,
    onconfirm: undefined,
    ontimeout: undefined,
    onactivate: undefined,
    classInitial: '',
    classWaiting: '',
    debug: false
  };

})(window.fossil);
Changes to src/fossil.fetch.js.
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

   - onload: callback(responseData) (default = output response to
   the console).

   - onerror: callback(XHR onload event | exception)
   (default = event or exception to the console).

   - method: 'POST' | 'GET' (default = 'GET')

   - payload: anything acceptable by XHR2.send(ARG) (DOMString,
   Document, FormData, Blob, File, ArrayBuffer), or a plain object or
   array, either of which gets JSON.stringify()'d. If payload is set
   then the method is automatically set to 'POST'. If an object/array
   is converted to JSON, the contentType option is automatically set
   to 'application/json'. By default XHR2 will set the content type







|







21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

   - onload: callback(responseData) (default = output response to
   the console).

   - onerror: callback(XHR onload event | exception)
   (default = event or exception to the console).

   - method: 'POST' | 'GET' (default = 'GET'). CASE SENSITIVE!

   - payload: anything acceptable by XHR2.send(ARG) (DOMString,
   Document, FormData, Blob, File, ArrayBuffer), or a plain object or
   array, either of which gets JSON.stringify()'d. If payload is set
   then the method is automatically set to 'POST'. If an object/array
   is converted to JSON, the contentType option is automatically set
   to 'application/json'. By default XHR2 will set the content type
58
59
60
61
62
63
64

65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
   overwrite those members to provide default implementations suitable
   for the page's use.

   Returns this object, noting that the XHR request is asynchronous,
   and still in transit (or has yet to be sent) when that happens.
*/
window.fossil.fetch = function f(uri,opt){

  if(!f.onerror){
    f.onerror = function(e/*event or exception*/){
      console.error("Ajax error:",e);
      if(e instanceof Error){
        fossil.error('Exception:',e);
      }
      else if(e.originalTarget && e.originalTarget.responseType==='text'){
        const txt = e.originalTarget.responseText;
        try{
          /* The convention from the /filepage_xyz routes is to
             return error responses in JSON form if possible:
             {error: "..."}
          */
          const j = JSON.parse(txt);
          console.error("Error JSON:",j);
          if(j.error){ fossil.error(j.error) };
        }catch(e){/* Try harder */
          fossil.error(txt)
        }
      }
    };
    f.onload = (r)=>console.debug('ajax response:',r);
  }
  if('/'===uri[0]) uri = uri.substr(1);
  if(!opt) opt = {};
  else if('function'===typeof opt) opt={onload:opt};
  if(!opt.onload) opt.onload = f.onload;
  if(!opt.onerror) opt.onerror = f.onerror;
  let payload = opt.payload, jsonResponse = false;
  if(payload){
    opt.method = 'POST';
    if(!(payload instanceof FormData)
       && !(payload instanceof Document)
       && !(payload instanceof Blob)
       && !(payload instanceof File)
       && !(payload instanceof ArrayBuffer)
       && ('object'===typeof payload
           || payload instanceof Array)){
      payload = JSON.stringify(payload);
      opt.contentType = 'application/json';
    }
  }
  const url=[window.fossil.repoUrl(uri,opt.urlParams)],
        x=new XMLHttpRequest();
  if('POST'===opt.method && 'string'===typeof opt.contentType){
    x.setRequestHeader('Content-Type',opt.contentType);
  }
  x.open(opt.method||'GET', url.join(''), true);
  if('json'===opt.responseType){
    /* 'json' is an extension to the supported XHR.responseType







>




|










|

|











|












|







58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
   overwrite those members to provide default implementations suitable
   for the page's use.

   Returns this object, noting that the XHR request is asynchronous,
   and still in transit (or has yet to be sent) when that happens.
*/
window.fossil.fetch = function f(uri,opt){
  const F = fossil;
  if(!f.onerror){
    f.onerror = function(e/*event or exception*/){
      console.error("Ajax error:",e);
      if(e instanceof Error){
        F.error('Exception:',e);
      }
      else if(e.originalTarget && e.originalTarget.responseType==='text'){
        const txt = e.originalTarget.responseText;
        try{
          /* The convention from the /filepage_xyz routes is to
             return error responses in JSON form if possible:
             {error: "..."}
          */
          const j = JSON.parse(txt);
          console.error("Error JSON:",j);
          if(j.error){ F.error(j.error) };
        }catch(e){/* Try harder */
          F.error(txt)
        }
      }
    };
    f.onload = (r)=>console.debug('ajax response:',r);
  }
  if('/'===uri[0]) uri = uri.substr(1);
  if(!opt) opt = {};
  else if('function'===typeof opt) opt={onload:opt};
  if(!opt.onload) opt.onload = f.onload;
  if(!opt.onerror) opt.onerror = f.onerror;
  let payload = opt.payload, jsonResponse = false;
  if(undefined!==payload){
    opt.method = 'POST';
    if(!(payload instanceof FormData)
       && !(payload instanceof Document)
       && !(payload instanceof Blob)
       && !(payload instanceof File)
       && !(payload instanceof ArrayBuffer)
       && ('object'===typeof payload
           || payload instanceof Array)){
      payload = JSON.stringify(payload);
      opt.contentType = 'application/json';
    }
  }
  const url=[F.repoUrl(uri,opt.urlParams)],
        x=new XMLHttpRequest();
  if('POST'===opt.method && 'string'===typeof opt.contentType){
    x.setRequestHeader('Content-Type',opt.contentType);
  }
  x.open(opt.method||'GET', url.join(''), true);
  if('json'===opt.responseType){
    /* 'json' is an extension to the supported XHR.responseType
129
130
131
132
133
134
135
136
137
138
139
        opt.onload((jsonResponse && this.response)
                   ? JSON.parse(this.response) : this.response);
      }catch(e){
        if(opt.onerror) opt.onerror(e);
      }
    }
  }
  if(payload) x.send(payload);
  else x.send();
  return this;
};







|



130
131
132
133
134
135
136
137
138
139
140
        opt.onload((jsonResponse && this.response)
                   ? JSON.parse(this.response) : this.response);
      }catch(e){
        if(opt.onerror) opt.onerror(e);
      }
    }
  }
  if(undefined!==payload) x.send(payload);
  else x.send();
  return this;
};
Changes to src/fossil.tabs.js.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20




21
22



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39



40


41
42
43
44
45



















46
47
48
49
50
51
52
53
54
55
56

57

58

59
60
61
62
63
64

65
66
67
68
69
70
71
72
73
74
75
76
77
78
79





80
81
82
83
84
85
86

87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
"use strict";
(function(F){
  const E = (s)=>document.querySelector(s),
        EA = (s)=>document.querySelectorAll(s),
        D = F.dom;

  const stopEvent = function(e){
    e.preventDefault();
    e.stopPropagation();
  };

  /**
     Creates a TabManager. If passed an argument, it is
     passed to init().
  */
  const TabManager = function(domElem){
    this.e = {};
    if(domElem) this.init(domElem);
  };





  const tabArg = function(arg){
    if('string'===typeof arg) arg = E(arg);



    return arg;
  };

  const setVisible = function(e,yes){
    D[yes ? 'removeClass' : 'addClass'](e, 'hidden');
  };

  TabManager.prototype = {
    /**
       Initializes the tabs associated with the given tab container
       (DOM element or selector for a single element).

       The tab container must have an 'id' attribute. This function
       looks through the DOM for all elements which have
       data-tab-parent=thatId. For each one it creates a button to
       switch to that tab and moves the element into this.e.tabs.




       When it's done, it auto-selects the first tab.



       This method must only be called once per instance. TabManagers
       may be nested but may not share any tabs.

       Returns this object.



















    */
    init: function(container){
      container = tabArg(container);
      const cID = container.getAttribute('id');
      if(!cID){
        throw new Error("Tab container element is missing 'id' attribute.");
      }
      const c = this.e.container = container;
      this.e.tabBar = D.addClass(D.div(),'tab-bar');
      this.e.tabs = D.addClass(D.div(),'tabs');
      D.append(c, this.e.tabBar, this.e.tabs);

      const childs = EA('[data-tab-parent='+cID+']');

      childs.forEach((c)=>this.addTab(c));

      return this.switchToTab(this.e.tabs.firstChild);
    },
    /**
       For the given tab element or unique selector string, returns
       the button associated with that tab, or undefined if the
       argument does not match any current tab.

    */
    getButtonForTab: function(tab){
      tab = tabArg(tab);
      var i = -1;
      this.e.tabs.childNodes.forEach(function(e,n){
        if(e===tab) i = n;
      });
      return i>=0 ? this.e.tabBar.childNodes[i] : undefined;
    },
    /**
       Adds the given DOM element or unique selector as the next
       tab in the tab container, adding a button to switch to
       the tab. Returns this object.
    */
    addTab: function(tab){





      tab = tabArg(tab);
      tab.remove();
      D.append(this.e.tabs, D.addClass(tab,'tab-panel'));
      const lbl = tab.dataset.tabLabel || 'Tab #'+this.e.tabs.childNodes.length;
      const btn = D.button(lbl);
      D.append(this.e.tabBar,btn);
      const self = this;

      btn.addEventListener('click',function(e){
        //stopEvent(e);
        self.switchToTab(tab);
      }, false);
      return this;
    },
    /**
       If the given DOM element or unique selector is one of this
       object's tabs, the UI makes that tab the currently-visible
       one. Returns this object.
    */
    switchToTab: function(tab){
      tab = tabArg(tab);
      const self = this;
      this.e.tabs.childNodes.forEach((e,ndx)=>{
        const btn = this.e.tabBar.childNodes[ndx];
        if(e===tab){
          setVisible(e, true);
          D.addClass(btn,'selected');
        }else{

|




<
<
<
<
<









>
>
>
>
|

>
>
>

















>
>
>
|
>
>


|


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











>
|
>
|
>
|


|
|
|
>


|











|
>
>
>
>
>



|


|
>
|
<
<
<



|
|
|


|







1
2
3
4
5
6





7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
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
"use strict";
(function(F/*fossil object*/){
  const E = (s)=>document.querySelector(s),
        EA = (s)=>document.querySelectorAll(s),
        D = F.dom;






  /**
     Creates a TabManager. If passed an argument, it is
     passed to init().
  */
  const TabManager = function(domElem){
    this.e = {};
    if(domElem) this.init(domElem);
  };

  /**
     Internal helper to normalize a method argument
     to a tab element.
  */
  const tabArg = function(arg,tabMgr){
    if('string'===typeof arg) arg = E(arg);
    else if(tabMgr && 'number'===typeof arg && arg>=0){
      arg = tabMgr.e.tabs.childNodes[arg];
    }
    return arg;
  };

  const setVisible = function(e,yes){
    D[yes ? 'removeClass' : 'addClass'](e, 'hidden');
  };

  TabManager.prototype = {
    /**
       Initializes the tabs associated with the given tab container
       (DOM element or selector for a single element).

       The tab container must have an 'id' attribute. This function
       looks through the DOM for all elements which have
       data-tab-parent=thatId. For each one it creates a button to
       switch to that tab and moves the element into this.e.tabs.

       The label for each tab is set by the data-tab-label attribute
       of each element, defaulting to something not terribly useful.

       When it's done, it auto-selects the first tab unless a tab has
       a truthy numeric value in its data-tab-select attribute, in
       which case the last tab to have such a property is selected.

       This method must only be called once per instance. TabManagers
       may be nested but may not share any tabs instances.

       Returns this object.

       DOM elements of potential interest to users:

       this.e.container = the outermost container element.

       this.e.tabBar = the button bar

       this.e.tabs = the parent for all of the tab elements.

       It is legal, within reason, to manipulate these a bit, in
       particular this.e.container, e.g. by adding more children to
       it. Do not remove elements from the tabs or tabBar, however, or
       the tab state may get sorely out of sync.

       CSS classes: the container element has whatever class(es) the
       client sets on. this.e.tabBar gets the 'tab-bar' class and
       this.e.tabs gets the 'tabs' class. It's hypothetically possible
       to move the tabs to either side or the bottom using only CSS,
       but it's never been tested.
    */
    init: function(container){
      container = tabArg(container);
      const cID = container.getAttribute('id');
      if(!cID){
        throw new Error("Tab container element is missing 'id' attribute.");
      }
      const c = this.e.container = container;
      this.e.tabBar = D.addClass(D.div(),'tab-bar');
      this.e.tabs = D.addClass(D.div(),'tabs');
      D.append(c, this.e.tabBar, this.e.tabs);
      let selectIndex = 0;
      EA('[data-tab-parent='+cID+']').forEach((c,n)=>{
        if(+c.dataset.tabSelect) selectIndex=n;
        this.addTab(c);
      });
      return this.switchToTab(selectIndex);
    },
    /**
       For the given tab element, unique selector string, or integer
       (0-based tab number), returns the button associated with that
       tab, or undefined if the argument does not match any current
       tab.
    */
    getButtonForTab: function(tab){
      tab = tabArg(tab,this);
      var i = -1;
      this.e.tabs.childNodes.forEach(function(e,n){
        if(e===tab) i = n;
      });
      return i>=0 ? this.e.tabBar.childNodes[i] : undefined;
    },
    /**
       Adds the given DOM element or unique selector as the next
       tab in the tab container, adding a button to switch to
       the tab. Returns this object.
    */
    addTab: function f(tab){
      if(!f.click){
        f.click = function(e){
         e.target.$manager.switchToTab(e.target.$tab);
        };
      }
      tab = tabArg(tab);
      tab.remove();
      D.append(this.e.tabs, D.addClass(tab,'tab-panel'));
      const lbl = tab.dataset.tabLabel || 'Tab #'+(this.e.tabs.childNodes.length-1);
      const btn = D.button(lbl);
      D.append(this.e.tabBar,btn);
      btn.$manager = this;
      btn.$tab = tab;
      btn.addEventListener('click', f.click, false);



      return this;
    },
    /**
       If the given DOM element, unique selector, or integer (0-based
       tab number) is one of this object's tabs, the UI makes that tab
       the currently-visible one. Returns this object.
    */
    switchToTab: function(tab){
      tab = tabArg(tab,this);
      const self = this;
      this.e.tabs.childNodes.forEach((e,ndx)=>{
        const btn = this.e.tabBar.childNodes[ndx];
        if(e===tab){
          setVisible(e, true);
          D.addClass(btn,'selected');
        }else{