Fossil

Check-in [f1b2e509]
Login

Check-in [f1b2e509]

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

Overview
Comment:fileedit: now gets the file perms via the response header and updates the Is Executable checkbox accordingly. Similary, mimetype is now harvested from the response headers and is used, in place of file extensions, for the "is this a wiki page?" determination. Added fossil-level event infrastructure to allow pages to communicate page-specific events to skin-injected/user-supplied JS code, and fileedit now emits a fileedit-file-loaded event when it loads a new file. Replaced old uses of the term "save" with "commit", per forum feedback.
Downloads: Tarball | ZIP archive | SQL archive
Timelines: family | ancestors | descendants | both | fileedit-ajaxify
Files: files | file ages | folders
SHA3-256: f1b2e509e770b4be664c6b42ae022eab43861f6da244db795eba81ae6de42b07
User & Date: stephan 2020-05-14 03:00:31
Context
2020-05-14
03:39
The filename/version block on the first fileedit tab is now generated completely dynamically, rather than being filled out with '???' with broken links before a file is loaded. ... (check-in: 371b162f user: stephan tags: fileedit-ajaxify)
03:00
fileedit: now gets the file perms via the response header and updates the Is Executable checkbox accordingly. Similary, mimetype is now harvested from the response headers and is used, in place of file extensions, for the "is this a wiki page?" determination. Added fossil-level event infrastructure to allow pages to communicate page-specific events to skin-injected/user-supplied JS code, and fileedit now emits a fileedit-file-loaded event when it loads a new file. Replaced old uses of the term "save" with "commit", per forum feedback. ... (check-in: f1b2e509 user: stephan tags: fileedit-ajaxify)
2020-05-13
16:44
Minor doc fix. ... (check-in: ae594780 user: stephan tags: fileedit-ajaxify)
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/fileedit.c.
1205
1206
1207
1208
1209
1210
1211









1212
1213
1214
1215
1216
1217
1218
  content_get(frid, &content);
  if(0==zMime){
    if(looks_like_binary(&content)){
      zMime = "application/octet-stream";
    }else{
      zMime = "text/plain";
    }









  }
  cgi_set_content_type(zMime);
  cgi_set_content(&content);
}

/*
** AJAX route /fileedit?ajax=preview







>
>
>
>
>
>
>
>
>







1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
  content_get(frid, &content);
  if(0==zMime){
    if(looks_like_binary(&content)){
      zMime = "application/octet-stream";
    }else{
      zMime = "text/plain";
    }
  }
  { /* Send the is-exec bit via response header so that the UI can be
    ** updated to account for that. */
    int fperm = 0;
    char * zFuuid = fileedit_file_uuid(zFilename, vid, &fperm);
    const char * zPerm = mfile_permint_mstring(fperm);
    assert(zFuuid);
    cgi_printf_header("x-fileedit-file-perm:%s\r\n", zPerm);
    fossil_free(zFuuid);
  }
  cgi_set_content_type(zMime);
  cgi_set_content(&content);
}

/*
** AJAX route /fileedit?ajax=preview
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
    case FE_RENDER_HTML_INLINE: zRenderMode = "htmlInline"; break;
    case FE_RENDER_HTML_IFRAME: zRenderMode = "htmlIframe"; break;
    case FE_RENDER_PLAIN_TEXT: zRenderMode = "text"; break;
    case FE_RENDER_GUESS:
      assert(!"cannot happen");
  }
  if(zRenderMode!=0){
    cgi_printf_header("X-fileedit-render-mode: %s\r\n", zRenderMode);
  }
}

/*
** AJAX route /fileedit?ajax=diff
**
** Required query parameters:







|







1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
    case FE_RENDER_HTML_INLINE: zRenderMode = "htmlInline"; break;
    case FE_RENDER_HTML_IFRAME: zRenderMode = "htmlIframe"; break;
    case FE_RENDER_PLAIN_TEXT: zRenderMode = "text"; break;
    case FE_RENDER_GUESS:
      assert(!"cannot happen");
  }
  if(zRenderMode!=0){
    cgi_printf_header("x-fileedit-render-mode: %s\r\n", zRenderMode);
  }
}

/*
** AJAX route /fileedit?ajax=diff
**
** Required query parameters:
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
     "data-tab-label='Commit'"
     ">");
  {
    /******* Commit flags/options *******/
    CX("<div class='fileedit-options flex-container flex-row'>");
    style_labeled_checkbox("cb-dry-run",
                           "dry_run", "Dry-run?", "1", 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.");
    style_labeled_checkbox("cb-allow-fork",
                           "allow_fork", "Allow fork?", "1",
                           cimi.flags & CIMINI_ALLOW_FORK,
                           "Allow saving to create a fork?");
    style_labeled_checkbox("cb-allow-older",
                           "allow_older", "Allow older?", "1",
                           cimi.flags & CIMINI_ALLOW_OLDER,
                           "Allow saving against a parent version "
                           "which has a newer timestamp?");
    style_labeled_checkbox("cb-exec-bit",
                           "exec_bit", "Executable?", "1",







|
|
|
|



|







1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
     "data-tab-label='Commit'"
     ">");
  {
    /******* Commit flags/options *******/
    CX("<div class='fileedit-options flex-container flex-row'>");
    style_labeled_checkbox("cb-dry-run",
                           "dry_run", "Dry-run?", "1", 1,
                           "In dry-run mode, the Commit button performs"
                           "all work needed for committing changes but "
                           "then rolls back the transaction, and thus "
                           "does not really commit.");
    style_labeled_checkbox("cb-allow-fork",
                           "allow_fork", "Allow fork?", "1",
                           cimi.flags & CIMINI_ALLOW_FORK,
                           "Allow committing to create a fork?");
    style_labeled_checkbox("cb-allow-older",
                           "allow_older", "Allow older?", "1",
                           cimi.flags & CIMINI_ALLOW_OLDER,
                           "Allow saving against a parent version "
                           "which has a newer timestamp?");
    style_labeled_checkbox("cb-exec-bit",
                           "exec_bit", "Executable?", "1",
Changes to src/fossil.bootstrap.js.
272
273
274
275
276
277
278










































279
280
            (r)=>eTo[asText ? 'textContent' : 'innerHTML'] = r||''
          );
        }, false
      );
    });
    return this;
  };











































})(window);







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


272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
            (r)=>eTo[asText ? 'textContent' : 'innerHTML'] = r||''
          );
        }, false
      );
    });
    return this;
  };

  /**
     Adds a listener for fossil-level custom events. Events are
     delivered to their callbacks as CustomEvent objects with a
     'detail' property holding the event's app-level data.

     The exact events fired differ by page, and not all pages trigger
     events.

     Pedantic sidebar: the custom event's 'target' property is an
     unspecified DOM element. Clients must not rely on its value being
     anything specific or useful.

     Returns this object.
  */
  F.page.addEventListener = function f(eventName, callback){
    if(!f.proxy){
      f.proxy = document.createElement('span');
    }
    f.proxy.addEventListener(eventName, callback, false);
    return this;
  };

  /**
     Internal. Dispatches a new CustomEvent to all listeners
     registered for the given eventName via
     fossil.page.addEventListener(), passing on a new CustomEvent
     with a 'detail' property equal to the 2nd argument's
     value. Returns this object.
   */
  F.page.dispatchEvent = function(eventName, eventDetail){
    if(this.addEventListener.proxy){
      try{
        this.addEventListener.proxy.dispatchEvent(
          new CustomEvent(eventName,{detail: eventDetail})
        );
      }catch(e){
        console.error(eventName,"event listener threw:",e);
      }
    }
    return this;
  };

})(window);
Changes to src/fossil.page.fileedit.js.
1
2
3
4
5




















6
7
8
9
10
11
12
(function(F/*the fossil object*/){
  "use strict";
  /**
     Code for the /filepage app. Requires that the fossil JS
     bootstrapping is complete and fossil.fetch() has been installed.




















  */
  const E = (s)=>document.querySelector(s),
        D = F.dom,
        P = F.page;

  /**
     Widget for the checkin/file selection list.





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







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
(function(F/*the fossil object*/){
  "use strict";
  /**
     Code for the /filepage app. Requires that the fossil JS
     bootstrapping is complete and fossil.fetch() has been installed.

     Custom events, handled via fossil.page.addEventListener():

     - 'fileedit-file-loaded': passes on information when it loads a
     file, in the form of an object:

     {
     filename: string,
     checkin: UUID string,
     isExe: bool,
     mimetype: mimetype stringas determined by the fossil server.
     }

     The fossil.page.value() method gets or sets the current file
     content for the page. Hypothetically, this can be overridden by
     skin-level JS in order to use a custom 3rd-party editing widget
     in place of the built-in textarea, but that is as yet untested.
     In order to do so the client would need to replace DOM element
     #fileedit-content-editor with their custom widget.

  */
  const E = (s)=>document.querySelector(s),
        D = F.dom,
        P = F.page;

  /**
     Widget for the checkin/file selection list.
185
186
187
188
189
190
191

192
193
194
195
196
197
198
      btnReload: E("#fileedit-tab-content > .fileedit-options > "
                   +"button.fileedit-content-reload"),
      selectPreviewMode: E('#select-preview-mode select'),
      selectHtmlEmsWrap: E('#select-preview-html-ems'),
      selectEolWrap:  E('#select-preview-html-ems'),
      cbLineNumbersWrap: E('#cb-line-numbers'),
      cbAutoPreview: E('#cb-preview-autoupdate > input[type=checkbox]'),

      tabs:{
        content: E('#fileedit-tab-content'),
        preview: E('#fileedit-tab-preview'),
        diff: E('#fileedit-tab-diff'),
        commit: E('#fileedit-tab-commit')
      }
    };







>







205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
      btnReload: E("#fileedit-tab-content > .fileedit-options > "
                   +"button.fileedit-content-reload"),
      selectPreviewMode: E('#select-preview-mode select'),
      selectHtmlEmsWrap: E('#select-preview-html-ems'),
      selectEolWrap:  E('#select-preview-html-ems'),
      cbLineNumbersWrap: E('#cb-line-numbers'),
      cbAutoPreview: E('#cb-preview-autoupdate > input[type=checkbox]'),
      cbIsExe: E('input[type=checkbox][name=exec_bit]'),
      tabs:{
        content: E('#fileedit-tab-content'),
        preview: E('#fileedit-tab-preview'),
        diff: E('#fileedit-tab-diff'),
        commit: E('#fileedit-tab-commit')
      }
    };
297
298
299
300
301
302
303








304
305
306
307
308
309
310
311
312
313

314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
        }, false
      );
      selectFontSize.dispatchEvent(
        // Force UI update
        new Event('change',{target:selectFontSize})
      );
    }








  }, false)/*onload event handler*/;

  /**
     Getter (if called with no args) or setter (if passed an arg) for
     the current file content. We use a function, rather than direct
     access so that clients can hypothetically swap out this method
     from their skin in order to facilitate plugging-in of fancy
     3rd-party editor widgets.

     The setter form returns this object.

  */
  P.value = function(){
    if(0===arguments.length){
      return this.e.taEditor.value;
    }else{
      this.e.taEditor.value = arguments[0];
      return this;
    }
  };

  /**
     If either of...

     - P.previewModes.current==='wiki'

     - P.previewModes.current==='guess' AND the currently-loaded
     file has an extension of (md|wiki)

     ... then this function updates the document's base.href to a
     repo-relative /doc/{{this.finfo.checkin}}/{{directory part of
     this.finfo.filename}}/

     If neither of those conditions applies, this is a no-op.
  */
  P.baseHrefForFile = function f(){
    const fn = this.finfo ? this.finfo.filename : undefined;
    if(!fn) return this;
    if(!f.rxWiki){
      f.rxWiki = /\.(wiki|md)$/i;
    }
    if('wiki'===P.previewModes.current
       || ('guess'===P.previewModes.current
           && f.rxWiki.test(fn))){
      const a = fn.split('/');
      a.pop();
      this.base.tag.href = F.repoUrl(
        'doc/'+F.hashDigits(this.finfo.checkin)
          +'/'+(a.length ? a.join('/')+'/' : '')
      );
    }







>
>
>
>
>
>
>
>





|
|
|

|
>















|
|










|
|



|







318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
        }, false
      );
      selectFontSize.dispatchEvent(
        // Force UI update
        new Event('change',{target:selectFontSize})
      );
    }

    if(0){ // only for testing
      P.addEventListener(
        'fileedit-file-loaded',
        (e)=>console.debug('fileedit-file-loaded ==>',e)
      );
    }

  }, false)/*onload event handler*/;

  /**
     Getter (if called with no args) or setter (if passed an arg) for
     the current file content. We use a function, rather than direct
     access, so that clients can hypothetically swap out this method
     from their skin in order to facilitate plugging-in of a fancy
     3rd-party editor widget.

     The setter form returns this object, and re-implementations must
     do the same.
  */
  P.value = function(){
    if(0===arguments.length){
      return this.e.taEditor.value;
    }else{
      this.e.taEditor.value = arguments[0];
      return this;
    }
  };

  /**
     If either of...

     - P.previewModes.current==='wiki'

     - P.previewModes.current==='guess' AND the currently-loaded file
     has a mimetype of "text/x-fossil-wiki" or "text/x-markdown".

     ... then this function updates the document's base.href to a
     repo-relative /doc/{{this.finfo.checkin}}/{{directory part of
     this.finfo.filename}}/

     If neither of those conditions applies, this is a no-op.
  */
  P.baseHrefForFile = function f(){
    const fn = this.finfo ? this.finfo.filename : undefined;
    if(!fn) return this;
    if(!f.wikiMimeTypes){
      f.wikiMimeTypes = ["text/x-fossil-wiki", "text/x-markdown"];
    }
    if('wiki'===P.previewModes.current
       || ('guess'===P.previewModes.current
           && f.wikiMimeTypes.indexOf(this.finfo.mimetype)>=0)){
      const a = fn.split('/');
      a.pop();
      this.base.tag.href = F.repoUrl(
        'doc/'+F.hashDigits(this.finfo.checkin)
          +'/'+(a.length ? a.join('/')+'/' : '')
      );
    }
434
435
436
437
438
439
440
441


442
443


444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459

460
461
462
463


464



465
466
467
468
469
470
471
  const affirmHasFile = function(){
    if(!P.finfo) F.error("No file is loaded.");
    return !!P.finfo;
  };

  /**
     loadFile() loads (file,checkinVersion) and updates the relevant
     UI elements to reflect the loaded state.



     Returns this object, noting that the load is async.


  */
  P.loadFile = function(file,rev){
    if(0===arguments.length){
      if(!affirmHasFile()) return this;
      file = this.finfo.filename;
      rev = this.finfo.checkin;
    }
    delete this.finfo;
    const self = this;
    F.message("Loading content...");
    F.fetch('fileedit',{
      urlParams: {
        ajax: 'content',
        filename:file,
        checkin:rev
      },

      onload:(r)=>{
        F.message('Loaded content.');
        self.value(r);
        self.updateVersion(file,rev);


        self.tabs.switchToTab(self.e.tabs.content);



      }
    });
    return this;
  };

  /**
     Fetches the page preview based on the contents and settings of







|
>
>

|
>
>
















>
|

<

>
>

>
>
>







464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496

497
498
499
500
501
502
503
504
505
506
507
508
509
510
  const affirmHasFile = function(){
    if(!P.finfo) F.error("No file is loaded.");
    return !!P.finfo;
  };

  /**
     loadFile() loads (file,checkinVersion) and updates the relevant
     UI elements to reflect the loaded state. If passed no arguments
     then it re-uses the values from the currently-loaded file
     (becoming a no-op if no file is loaded).

     Returns this object, noting that the load is async. After loading
     it triggers a 'fileedit-file-loaded' event, passing it
     this.finfo.
  */
  P.loadFile = function(file,rev){
    if(0===arguments.length){
      if(!affirmHasFile()) return this;
      file = this.finfo.filename;
      rev = this.finfo.checkin;
    }
    delete this.finfo;
    const self = this;
    F.message("Loading content...");
    F.fetch('fileedit',{
      urlParams: {
        ajax: 'content',
        filename:file,
        checkin:rev
      },
      responseHeaders: ['x-fileedit-file-perm', 'content-type'],
      onload:(r,headers)=>{
        F.message('Loaded content.');

        self.updateVersion(file,rev);
        self.finfo.isExe = ('x'===headers['x-fileedit-file-perm']);
        self.finfo.mimetype = headers['content-type'].split(';').shift();
        self.tabs.switchToTab(self.e.tabs.content);
        self.e.cbIsExe.checked = self.finfo.isExe;
        self.value(r);
        self.dispatchEvent('fileedit-file-loaded', self.finfo);
      }
    });
    return this;
  };

  /**
     Fetches the page preview based on the contents and settings of
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
        fossil.fetch.onerror(e);
        callback("Error fetching preview: "+e);
      }
    });
    return this;
  };

  
  /**
     Fetches the content diff based on the contents and settings of this
     page's input fields, and updates the UI with the diff view.

     Returns this object, noting that the operation is async.
  */
  P.diff = function f(sbs){
    if(!affirmHasFile()) return this;
    const content = this.value(),
          self = this;







<

|
|







561
562
563
564
565
566
567

568
569
570
571
572
573
574
575
576
577
        fossil.fetch.onerror(e);
        callback("Error fetching preview: "+e);
      }
    });
    return this;
  };


  /**
     Fetches the content diff based on the contents and settings of
     this page's input fields, and updates the UI with the diff view.

     Returns this object, noting that the operation is async.
  */
  P.diff = function f(sbs){
    if(!affirmHasFile()) return this;
    const content = this.value(),
          self = this;
642
643
644
645
646
647
648
649
650
651
      urlParams: {ajax: 'commit'},
      payload: fd,
      responseType: 'json',
      onload: f.updateView
    });
    return this;
  };

  
})(window.fossil);







|
<

680
681
682
683
684
685
686
687

688
      urlParams: {ajax: 'commit'},
      payload: fd,
      responseType: 'json',
      onload: f.updateView
    });
    return this;
  };
  

})(window.fossil);