Fossil

Check-in [33ffe5762b]
Login

Check-in [33ffe5762b]

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

Overview
Comment:Completely overhauled the /fileedit layout, using a homebrew tabbed interface.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | fileedit-ajaxify
Files: files | file ages | folders
SHA3-256: 33ffe5762bef215a33c802bedd28a0ef46aaec1b3b515cf8bb74e9a5841d0918
User & Date: stephan 2020-05-05 16:51:07.716
Context
2020-05-05
17:23
Doc updates and corrections. ... (check-in: e7659e7265 user: stephan tags: fileedit-ajaxify)
16:51
Completely overhauled the /fileedit layout, using a homebrew tabbed interface. ... (check-in: 33ffe5762b user: stephan tags: fileedit-ajaxify)
11:51
/fileedit_content now only uses application/octet-stream for files which explicitly have that type via mimetype_by_name() or which look like binary content, falling back to text/plain, per suggestion in the discussion thread. ... (check-in: 4270ecb3a2 user: stephan tags: fileedit-ajaxify)
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/default_css.txt.
864
865
866
867
868
869
870
871
872
873
874

875
876
877
878
879
880
881
// }
// .fileedit-XXX => /fileedit page
form.fileedit textarea {
  font-family: monospace;
  width: 100%;
}
form.fileedit fieldset {
  margin: 0.5em 0 0 0;
  border-radius: 0.5em;
  border-color: inherit;
  border-width: 1px;

}
form.fileedit fieldset > legend {
  margin: 0 0 0 1em;
  padding: 0 0.5em 0 0.5em;
}
form.fileedit fieldset > div {
  margin: 0 0.25em 0.25em 0.25em;







|



>







864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
// }
// .fileedit-XXX => /fileedit page
form.fileedit textarea {
  font-family: monospace;
  width: 100%;
}
form.fileedit fieldset {
  margin: 0.5em 0 0.5em 0;
  border-radius: 0.5em;
  border-color: inherit;
  border-width: 1px;
  font-size: 85%;
}
form.fileedit fieldset > legend {
  margin: 0 0 0 1em;
  padding: 0 0.5em 0 0.5em;
}
form.fileedit fieldset > div {
  margin: 0 0.25em 0.25em 0.25em;
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922

923
924
925
926
927
928
929
930
931
932


933
934
935
936
937
938
939
  overflow: auto;
  white-space: pre;
}
div.fileedit-preview {
  margin: 0;
  padding: 0;
}
.fileedit-preview > div:first-child {
  margin: 1em 0 0 0;
  border-bottom: 1px dashed;
}
div.fileedit-diff {
  margin: 0;
  padding: 0;
}
.fileedit-diff > div:first-child {
  border-bottom: 1px dashed;

}
#fossil-status-bar {
  display: block;
  font-family: monospace;
  border-width: 1px;
  border-style: inset;
  border-color: inherit;
  min-height: 1.5em;
  font-size: 1.2em;
  padding: 0.2em;


}
#fossil-status-bar.error {
  color: darkred;
  background: yellow;
}
.input-with-label {
  border: 1px inset #808080;







<
<
<
<
|



|
|
>










>
>







907
908
909
910
911
912
913




914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
  overflow: auto;
  white-space: pre;
}
div.fileedit-preview {
  margin: 0;
  padding: 0;
}




div.fileedit-tab-diff-wrapper {
  margin: 0;
  padding: 0;
}
#fileedit-comment {
  width: 100%;
  font-family: monospace;
}
#fossil-status-bar {
  display: block;
  font-family: monospace;
  border-width: 1px;
  border-style: inset;
  border-color: inherit;
  min-height: 1.5em;
  font-size: 1.2em;
  padding: 0.2em;
  margin: 0.25em 0;
  flex: 0 0 auto;
}
#fossil-status-bar.error {
  color: darkred;
  background: yellow;
}
.input-with-label {
  border: 1px inset #808080;
965
966
967
968
969
970
971


972
973



974
975
976
977
978
979
980
981
982
983
984
985
986
987
988









































    vertical-align: sub;
}
.input-with-label > span {
    margin: 0 0.25em 0 0.25em;
    vertical-align: middle;
}
.hidden {


  display: none;
}



.font-size-100 {
  font-size: 100%;
}
.font-size-125 {
  font-size: 125%;
}
.font-size-150 {
  font-size: 150%;
}
.font-size-175 {
  font-size: 175%;
}
.font-size-200 {
  font-size: 200%;
}
















































>
>
|

>
>
>















>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
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
    vertical-align: sub;
}
.input-with-label > span {
    margin: 0 0.25em 0 0.25em;
    vertical-align: middle;
}
.hidden {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
//.hidden {
//  display: none;
//}
.font-size-100 {
  font-size: 100%;
}
.font-size-125 {
  font-size: 125%;
}
.font-size-150 {
  font-size: 150%;
}
.font-size-175 {
  font-size: 175%;
}
.font-size-200 {
  font-size: 200%;
}
.tab-container {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: stretch;
}
.tab-container > #fossil-status-bar {
  margin-top: 0;
}
.tab-container > .tabs {
  padding: 0.25em;
  margin: 0;
  display: flex;
  flex-direction: column;
  border-width: 1px;
  border-style: outset;
  border-color: inherit;
}
.tab-container > .tabs > .tab-panel {
  align-self: stretch;
  flex: 10 1 auto;
  display: block;
}
.tab-container > .tab-bar {
  display: flex;
  flex-direction: row;
  flex: 1 10 auto;
  align-self: stretch;
  flex-wrap: wrap;
}
.tab-container > .tab-bar > button {
  border-radius: 0.5em 0.5em 0 0;
  margin: 0.5em 0.5em 0 0.5em;
  align-self: baseline;
}
.tab-container > .tab-bar > button.selected {
  font-style: italic;
  font-weight: bold;
  margin: 0 0.5em;
  text-decoration: underline;
}
Changes to src/fileedit.c.
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453

1454
1455
1456
1457
1458
1459
1460
}


/*
** Emits utility script code specific to the /fileedit page.
*/
static void fileedit_emit_page_script(){
  style_emit_script_tag(0);
  CX("%s\n", builtin_text("fossil.page.fileedit.js"));
  style_emit_script_tag(1);

}

/*
** WEBPAGE: fileedit
**
** EXPERIMENTAL and subject to change and removal at any time. The goal
** is to allow online edits of files.







|
<
|
>







1444
1445
1446
1447
1448
1449
1450
1451

1452
1453
1454
1455
1456
1457
1458
1459
1460
}


/*
** Emits utility script code specific to the /fileedit page.
*/
static void fileedit_emit_page_script(){
  style_emit_script_fetch();

  style_emit_script_tabs();
  style_emit_script_builtin("fossil.page.fileedit.js");
}

/*
** WEBPAGE: fileedit
**
** EXPERIMENTAL and subject to change and removal at any time. The goal
** is to allow online edits of files.
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542










1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564































































































































1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616

1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
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
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
  ** All errors which "could" have happened up to this point are of a
  ** degree which keep us from rendering the rest of the page, and
  ** thus have already caused us to skipped to the end of the page to
  ** render the errors. Any up-coming errors, barring malloc failure
  ** or similar, are not "that" fatal. We can/should continue
  ** rendering the page, then output the error message at the end.
  ********************************************************************/
  CX("<h1>Editing:</h1>");
  CX("<p class='fileedit-hint'>");
  CX("File: "
     "[<a id='finfo-link' href='#'>info</a>] "
     /* %R/finfo?name=%T&m=%!S */
     "<code id='finfo-file-name'>(loading)</code><br>");
  CX("Checkin Version: "
     "[<a id='r-link' href='#'>info</a>] "
     /* %R/info/%!S */
     "<code id='r-label'>(loading...)</code><br>"
     );
  CX("Permalink: <code>"
     "<a id='permalink' href='#'>(loading...)</a></code><br>"
     "(Clicking the permalink will reload the page and discard "
     "all edits!)",
     zFilename, cimi.zParentUuid,
     zFilename, cimi.zParentUuid);
  CX("</p>");
  CX("<p>This page is <em>NEW AND EXPERIMENTAL</em>. "
     "USE AT YOUR OWN RISK, preferably on a test "
     "repo.</p>\n");
  










  CX("<form action='#' method='POST' "
     "class='fileedit' id='fileedit-form' "
     "onsubmit='function(e){"
     "e.preventDefault(); e.stopPropagation(); return false;"
     "}'>\n");

  /******* Hidden fields *******/
  CX("<input type='hidden' name='r' value='%s'>",
     cimi.zParentUuid);
  CX("<input type='hidden' name='file' value='%T'>",
     zFilename);

  /******* Content *******/
  CX("<h3>File Content</h3>\n");
  CX("<textarea name='content' id='fileedit-content' "
     "rows='20' cols='80'>");
  CX("Loading...");
  CX("</textarea>\n");

  CX("<div id='fossil-status-bar'>Async. status messages will go "
     "here.</div>\n");
































































































































  /******* Flags/options *******/
  CX("<fieldset class='fileedit-options' id='options'>"
     "<legend>Options</legend><div>"
     /* Chrome does not sanely lay out multiple
     ** fieldset children after the <legend>, so
     ** a containing div is necessary. */);
  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.",
                         1);
  style_labeled_checkbox("cb-allow-fork",
                         "allow_fork", "Allow fork?", "1",
                         "Allow saving to create a fork?",
                         cimi.flags & CIMINI_ALLOW_FORK);
  style_labeled_checkbox("cb-allow-older",
                         "allow_older", "Allow older?", "1",
                         "Allow saving against a parent version "
                         "which has a newer timestamp?",
                         cimi.flags & CIMINI_ALLOW_OLDER);
  style_labeled_checkbox("cb-exec-bit",
                         "exec_bit", "Executable?", "1",
                         "Set the executable bit?",
                         PERM_EXE==cimi.filePerm);
  style_labeled_checkbox("cb-allow-merge-conflict",
                         "allow_merge_conflict",
                         "Allow merge conflict markers?", "1",
                         "Allow saving even if the content contains "
                         "what appear to be fossil merge conflict "
                         "markers?",
                         cimi.flags & CIMINI_ALLOW_MERGE_MARKER);
  style_labeled_checkbox("cb-prefer-delta",
                         "prefer_delta",
                         "Prefer delta manifest?", "1",
                         "Will create a delta manifest, instead of "
                         "baseline, if conditions are favorable to do "
                         "so. This option is only a suggestion.",
                         cimi.flags & CIMINI_PREFER_DELTA);
  style_select_list_int("select-eol-style",
                        "eol", "EOL Style",
                        "EOL conversion policy, noting that "
                        "form-processing may implicitly change the "
                        "line endings of the input.",
                        (cimi.flags & CIMINI_CONVERT_EOL_UNIX)
                        ? 1 : (cimi.flags & CIMINI_CONVERT_EOL_WINDOWS
                               ? 2 : 0),
                        "Inherit", 0,
                        "Unix", 1,
                        "Windows", 2,
                        NULL);

  style_select_list_int("select-font-size",
                        "editor_font_size", "Editor Font Size",
                        NULL/*tooltip*/,
                        100,
                        "100%", 100, "125%", 125,
                        "150%", 150, "175%", 175,
                        "200%", 200, NULL);

  CX("</div></fieldset>") /* end of checkboxes */;

  /******* Comment *******/
  CX("<a id='comment'></a>");
  CX("<fieldset><legend>Commit message</legend><div>");
  CX("<textarea name='comment' rows='3' cols='80' "
     "id='fileedit-comment'>");
  /* ^^^ adding the 'required' attribute means we cannot even submit
  ** for PREVIEW mode if it's empty :/. */
  if(blob_size(&cimi.comment)){
    CX("%h", blob_str(&cimi.comment));
  }
  CX("</textarea>\n");
  CX("<div class='fileedit-hint'>Comments use the Fossil wiki markup "
     "syntax.</div>\n"/*TODO: select for fossil/md/plain text*/);
  CX("</div></fieldset>\n");

  /******* Buttons *******/
  CX("<a id='buttons'></a>");
  CX("<fieldset class='fileedit-options'>"
     "<legend>Ask the server to...</legend><div>");
  CX("<button id='fileedit-btn-commit'>Commit</button>");
  CX("<button id='fileedit-btn-diffsbs'>Diff (SBS)</button>");
  CX("<button id='fileedit-btn-diffu'>Diff (Unified)</button>");
  CX("<button id='fileedit-btn-preview'>Preview</button>");
  /* Default preview rendering mode selection... */
  previewRenderMode = fileedit_render_mode_for_mimetype(zFileMime);
  style_select_list_int("select-preview-mode",
                        "preview_render_mode",
                        "Preview Mode",
                        "Preview mode format.",
                        previewRenderMode,
                        "Guess", FE_RENDER_GUESS,
                        "Wiki/Markdown", FE_RENDER_WIKI,
                        "HTML (iframe)", FE_RENDER_HTML,
                        "Plain Text", FE_RENDER_PLAIN_TEXT,
                        NULL);
  /*
  ** Set up a JS-side mapping of the FE_RENDER_xyz values.  This is
  ** used for dynamically toggling certain UI components on and off.
  */





  blob_appendf(&endScript, "fossil.page.previewModes={"
               "guess: %d, %d: 'guess', wiki: %d, %d: 'wiki',"
               "html: %d, %d: 'html', text: %d, %d: 'text'"

               "};\n",
               FE_RENDER_GUESS, FE_RENDER_GUESS,
               FE_RENDER_WIKI, FE_RENDER_WIKI,
               FE_RENDER_HTML, FE_RENDER_HTML,
               FE_RENDER_PLAIN_TEXT, FE_RENDER_PLAIN_TEXT);



  /* Allow selection of HTML preview iframe height */
  previewHtmlHeight = atoi(PD("preview_html_ems","0"));
  if(!previewHtmlHeight){
    previewHtmlHeight = 40;




  }
  style_select_list_int("select-preview-html-ems",
                        "preview_html_ems",
                        "HTML Preview IFrame Height (EMs)",
                        "Height (in EMs) of the iframe used for "
                        "HTML preview",
                        previewHtmlHeight,
                        "", 20, "", 40,
                        "", 60, "", 80,
                        "", 100, NULL);
  /* Selection of line numbers for text preview */
  style_labeled_checkbox("cb-line-numbers",
                         "preview_ln",
                         "Add line numbers to plain-text previews?",
                         "1",
                         "If on, plain-text files (only) will get "
                         "line numbers added to the preview.",
                         P("preview_ln")!=0);

  CX("</div></fieldset>");

  /******* End of form *******/    
  CX("</form>\n");

  CX("<div id='ajax-target'>%s</div>"
     /* this is where preview/diff go */);
  
  /* Dynamically populate the editor... */
  blob_appendf(&endScript,
               "fossil.page.loadFile('%j','%j');",
               zFilename, cimi.zParentUuid);

end_footer:
  fossil_free(zFileUuid);
  if(stmt.pStmt){
    db_finalize(&stmt);
  }
  if(blob_size(&err)){
    CX("<div class='fileedit-error-report'>%s</div>",
       blob_str(&err));
  }
  blob_reset(&err);
  CheckinMiniInfo_cleanup(&cimi);
  style_emit_script_fetch();
  fileedit_emit_page_script();
  if(blob_size(&endScript)>0){
    style_emit_script_tag(0);
    CX("(function(){\n");
    CX("try{\n%b\n}catch(e){console.error('Exception:',e)}\n",
       &endScript);
    CX("})();");
    style_emit_script_tag(1);
  }
  db_end_transaction(0);
  style_footer();
}







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<



|
>
>
>
>
>
>
>
>
>
>
|
|
<
<
<







<
<
<
<
<
<
|

|

>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
|
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
|
|
|
>
>
>
>
>
|
<
<
>
|
<
<
<
<
|
>
>
|
<
<
|
>
>
>
>

<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<

|
|


<
<
<

















<












1514
1515
1516
1517
1518
1519
1520


















1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536



1537
1538
1539
1540
1541
1542
1543






1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
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
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735















1736
1737




















1738
1739
1740
1741
1742
1743
1744
1745
1746
1747


1748
1749




1750
1751
1752
1753


1754
1755
1756
1757
1758
1759

















1760
1761
1762
1763
1764



1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781

1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
  ** All errors which "could" have happened up to this point are of a
  ** degree which keep us from rendering the rest of the page, and
  ** thus have already caused us to skipped to the end of the page to
  ** render the errors. Any up-coming errors, barring malloc failure
  ** or similar, are not "that" fatal. We can/should continue
  ** rendering the page, then output the error message at the end.
  ********************************************************************/


















  CX("<p>This page is <em>NEW AND EXPERIMENTAL</em>. "
     "USE AT YOUR OWN RISK, preferably on a test "
     "repo.</p>\n");

  /*
  ** We don't strictly need a FORM because we manually cherry-pick and
  ** submit the form data, but it being in a form allows us to easily
  ** use the FormData type for serialization.
  **
  ** TODO?: we can almost certainly replace this element with a plain
  ** DIV, which would eliminate the event-handling hassles of trying
  ** to suppress the submit... but it would also eliminate the option
  ** of using HTML form field validation.
  */
  CX("<form action='#' method='POST' class='fileedit' "
     "id='fileedit-form'>");




  /******* Hidden fields *******/
  CX("<input type='hidden' name='r' value='%s'>",
     cimi.zParentUuid);
  CX("<input type='hidden' name='file' value='%T'>",
     zFilename);







  /* Status bar */
  CX("<div id='fossil-status-bar'>Async. status messages will go "
     "here.</div>\n"/* will be moved into the tab container via JS */);

  /* Main tab container... */
  CX("<div id='fileedit-tabs' class='tab-container'></div>");

  /***** File/version info tab *****/
  {
    CX("<div id='fileedit-tab-version' "
       "data-tab-parent='fileedit-tabs' "
       "data-tab-label='Version Info'"
       ">");
    CX("File: "
       "[<a id='finfo-link' href='#'>/finfo</a>] "
       /* %R/finfo?name=%T&m=%!S */
       "<code id='finfo-file-name'>(loading)</code><br>");
    CX("Checkin Version: "
       "[<a id='r-link' href='#'>/info</a>] "
       /* %R/info/%!S */
       "<code id='r-label'>(loading...)</code><br>"
       );
    CX("Permalink: <code>"
       "<a id='permalink' href='#'>(loading...)</a></code><br>"
       "(Clicking the permalink will reload the page and discard "
       "all edits!)",
       zFilename, cimi.zParentUuid,
       zFilename, cimi.zParentUuid);
    CX("</div>"/*#fileedit-tab-version*/);
  }
  
  /******* Content tab *******/
  {
    CX("<div id='fileedit-tab-content' "
       "data-tab-parent='fileedit-tabs' "
       "data-tab-label='File Content'"
       ">");
    CX("<textarea name='content' id='fileedit-content-editor' "
       "rows='20' cols='80'>");
    CX("Loading...");
    CX("</textarea>\n");
    CX("</div>"/*#tab-file-content*/);
  }

  /****** Preview tab ******/
  {
    CX("<div id='fileedit-tab-preview' "
       "data-tab-parent='fileedit-tabs' "
       "data-tab-label='Preview'"
       ">");

    CX("<fieldset class='fileedit-options'>"
       "<legend>Preview...</legend><div>");
    
    CX("<div class='preview-controls'>");
    CX("<button>Refresh</button>");
    /* Default preview rendering mode selection... */
    previewRenderMode = fileedit_render_mode_for_mimetype(zFileMime);
    style_select_list_int("select-preview-mode",
                          "preview_render_mode",
                          "Preview Mode",
                          "Preview mode format.",
                          previewRenderMode,
                          "Guess", FE_RENDER_GUESS,
                          "Wiki/Markdown", FE_RENDER_WIKI,
                          "HTML (iframe)", FE_RENDER_HTML,
                          "Plain Text", FE_RENDER_PLAIN_TEXT,
                          NULL);
    /*
    ** Set up a JS-side mapping of the FE_RENDER_xyz values.  This is
    ** used for dynamically toggling certain UI components on and off.
    */
    blob_appendf(&endScript, "fossil.page.previewModes={"
                 "guess: %d, %d: 'guess', wiki: %d, %d: 'wiki',"
                 "html: %d, %d: 'html', text: %d, %d: 'text'"
                 "};\n",
                 FE_RENDER_GUESS, FE_RENDER_GUESS,
                 FE_RENDER_WIKI, FE_RENDER_WIKI,
                 FE_RENDER_HTML, FE_RENDER_HTML,
                 FE_RENDER_PLAIN_TEXT, FE_RENDER_PLAIN_TEXT);
    /* Allow selection of HTML preview iframe height */
    previewHtmlHeight = atoi(PD("preview_html_ems","0"));
    if(!previewHtmlHeight){
      previewHtmlHeight = 40;
    }
    style_select_list_int("select-preview-html-ems",
                          "preview_html_ems",
                          "HTML Preview IFrame Height (EMs)",
                          "Height (in EMs) of the iframe used for "
                          "HTML preview",
                          previewHtmlHeight,
                          "", 20, "", 40,
                          "", 60, "", 80,
                          "", 100, NULL);
    /* Selection of line numbers for text preview */
    style_labeled_checkbox("cb-line-numbers",
                           "preview_ln",
                           "Add line numbers to plain-text previews?",
                           "1",
                           "If on, plain-text files (only) will get "
                           "line numbers added to the preview.",
                           P("preview_ln")!=0);
    CX("</div></fieldset>"/*.fileedit-options*/);
    CX("<div id='fileedit-tab-preview-wrapper'></div>");
    CX("</div>"/*#fileedit-tab-preview*/);
  }

  /****** Diff tab ******/
  {
    CX("<div id='fileedit-tab-diff' "
       "data-tab-parent='fileedit-tabs' "
       "data-tab-label='Diff'"
       ">");
    CX("<div id='fileedit-tab-diff-buttons'>"
       "<button class='sbs'>Side-by-side</button>"
       "<button class='unified'>Unified</button>"
       "</div>");
    CX("<div id='fileedit-tab-diff-wrapper'>"
       "Diffs will be shown here."
       "</div>");
    CX("</div>");
  }


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

  {
    /******* Flags/options *******/
    CX("<fieldset class='fileedit-options'>"
       "<legend>Options</legend><div>"
       /* Chrome does not sanely lay out multiple
       ** fieldset children after the <legend>, so
       ** a containing div is necessary. */);
    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.",
                           1);
    style_labeled_checkbox("cb-allow-fork",
                           "allow_fork", "Allow fork?", "1",
                           "Allow saving to create a fork?",
                           cimi.flags & CIMINI_ALLOW_FORK);
    style_labeled_checkbox("cb-allow-older",
                           "allow_older", "Allow older?", "1",
                           "Allow saving against a parent version "
                           "which has a newer timestamp?",
                           cimi.flags & CIMINI_ALLOW_OLDER);
    style_labeled_checkbox("cb-exec-bit",
                           "exec_bit", "Executable?", "1",
                           "Set the executable bit?",
                           PERM_EXE==cimi.filePerm);
    style_labeled_checkbox("cb-allow-merge-conflict",
                           "allow_merge_conflict",
                           "Allow merge conflict markers?", "1",
                           "Allow saving even if the content contains "
                           "what appear to be fossil merge conflict "
                           "markers?",
                           cimi.flags & CIMINI_ALLOW_MERGE_MARKER);
    style_labeled_checkbox("cb-prefer-delta",
                           "prefer_delta",
                           "Prefer delta manifest?", "1",
                           "Will create a delta manifest, instead of "
                           "baseline, if conditions are favorable to "
                           "do so. This option is only a suggestion.",
                           cimi.flags & CIMINI_PREFER_DELTA);
    style_select_list_int("select-eol-style",
                          "eol", "EOL Style",
                          "EOL conversion policy, noting that "
                          "form-processing may implicitly change the "
                          "line endings of the input.",
                          (cimi.flags & CIMINI_CONVERT_EOL_UNIX)
                          ? 1 : (cimi.flags & CIMINI_CONVERT_EOL_WINDOWS
                                 ? 2 : 0),
                          "Inherit", 0,
                          "Unix", 1,
                          "Windows", 2,
                          NULL);
#if 0
    style_select_list_int("select-font-size",
                          "editor_font_size", "Editor Font Size",
                          NULL/*tooltip*/,
                          100,
                          "100%", 100, "125%", 125,
                          "150%", 150, "175%", 175,
                          "200%", 200, NULL);
#endif















    CX("</div></fieldset>"/*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)){
      blob_appendf(&endScript,


                   "document.querySelector('#fileedit-comment').value="
                   "\"%h\";\n", blob_str(&cimi.comment));




    }
    CX("</input>\n");
    CX("<div class='fileedit-hint'>Comments use the Fossil wiki markup "
       "syntax.</div>\n"/*TODO: select for fossil/md/plain text*/);


    CX("</div></fieldset>\n"/*commit comment*/);
    CX("<div>"
       "<button id='fileedit-btn-commit'>Commit</button>"
       "</div>\n");
    CX("<div id='fileedit-manifest'></div>\n");
  }


















  CX("</div>"/*#fileedit-tab-commit*/);
  
  /******* End of form *******/    
  CX("</form>\n");



  
  /* Dynamically populate the editor... */
  blob_appendf(&endScript,
               "fossil.page.loadFile('%j','%j');",
               zFilename, cimi.zParentUuid);

end_footer:
  fossil_free(zFileUuid);
  if(stmt.pStmt){
    db_finalize(&stmt);
  }
  if(blob_size(&err)){
    CX("<div class='fileedit-error-report'>%s</div>",
       blob_str(&err));
  }
  blob_reset(&err);
  CheckinMiniInfo_cleanup(&cimi);

  fileedit_emit_page_script();
  if(blob_size(&endScript)>0){
    style_emit_script_tag(0);
    CX("(function(){\n");
    CX("try{\n%b\n}catch(e){console.error('Exception:',e)}\n",
       &endScript);
    CX("})();");
    style_emit_script_tag(1);
  }
  db_end_transaction(0);
  style_footer();
}
Changes to src/fossil.bootstrap.js.
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
  else{
    args.unshift('Fossil error:');
    console.error.apply(console,args);
  }
  return this;
};























/**
   repoUrl( repoRelativePath [,urlParams] )

   Creates a URL by prepending this.rootPath to the given path
   (which must be relative from the top of the site, without a
   leading slash). If urlParams is a string, it must be
   paramters encoded in the form "key=val&key2=val2...", WITHOUT
   a leading '?'. If it's an object, all of its properties get
   appended to the URL in that form.
*/
window.fossil.repoUrl = function(path,urlParams){
  if(!urlParams) return this.rootPath+path;
  const url=[this.rootPath,path];
  url.push('?');
  if('string'===typeof urlParams) url.push(urlParams);
  else if('object'===typeof urlParams){
    let k, i = 0;
    for( k in urlParams ){
      if(i++) url.push('&');
      url.push(k,'=',encodeURIComponent(urlParams[k]));
    }
  }
  return url.join('');
};







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
















<
|
<
<
<



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
  else{
    args.unshift('Fossil error:');
    console.error.apply(console,args);
  }
  return this;
};

/**
   For each property in the given object, its key/value are encoded
   for use as URL parameters and the combined string is
   returned. e.g. {a:1,b:2} encodes to "a=1&b=2".

   If the 2nd argument is an array, each encoded element is appended
   to that array and tgtArray is returned. The above object would be
   appended as ['a','=','1','&','b','=','2']. This form is used for
   building up parameter lists before join('')ing the array to create
   the result string.
*/
window.fossil.encodeUrlArgs = function(obj,tgtArray){
  if(!obj) return '';
  const a = (tgtArray instanceof Array) ? tgtArray : [];
  let k, i = 0;
  for( k in obj ){
    if(i++) a.push('&');
    a.push(encodeURIComponent(k),
           '=',encodeURIComponent(obj[k]));
  }
  return a===tgtArray ? a : a.join('');
};
/**
   repoUrl( repoRelativePath [,urlParams] )

   Creates a URL by prepending this.rootPath to the given path
   (which must be relative from the top of the site, without a
   leading slash). If urlParams is a string, it must be
   paramters encoded in the form "key=val&key2=val2...", WITHOUT
   a leading '?'. If it's an object, all of its properties get
   appended to the URL in that form.
*/
window.fossil.repoUrl = function(path,urlParams){
  if(!urlParams) return this.rootPath+path;
  const url=[this.rootPath,path];
  url.push('?');
  if('string'===typeof urlParams) url.push(urlParams);
  else if('object'===typeof urlParams){

    this.encodeUrlArgs(urlParams, url);



  }
  return url.join('');
};
Added src/fossil.dom.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
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
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
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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
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
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
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
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
"use strict";
(function(F/*fossil object*/){
  /**
     A collection of HTML DOM utilities to simplify, a bit,
     using the DOM API.
  */
  const argsToArray = (a)=>Array.prototype.slice.call(a,0);
  const isArray = (v)=>v instanceof Array;

  const dom = {
    create: function(elemType){
      return document.createElement(elemType);
    },
    createElemFactory: function(eType){
      return function(){
        return document.createElement(eType);
      };
    },
    remove: function(e){
      if(e.forEach){
        e.forEach(
          (x)=>x.parentNode.removeChild(x)
        );
      }else{
        e.parentNode.removeChild(e);
      }
      return e;
    },
    /**
       Removes all child DOM elements from the given element
       and returns that element.

       If e has a forEach method (is an array or DOM element
       collection), this function instead clears each element
       in the collection and returns e.
    */
    clearElement: function f(e){
      if(e.forEach){
        e.forEach((x)=>f(x));
        return e;
      }
      while(e.firstChild) e.removeChild(e.firstChild);
      return e;
    },
  }/* dom object */;

  /**
     Returns the result of splitting the given str on
     a run of spaces of (\s*,\s*).
  */
  dom.splitClassList = function f(str){
    if(!f.rx){
      f.rx = /(\s+|\s*,\s*)/;
    }
    return str ? str.split(f.rx) : [str];
  };
  
  dom.div = dom.createElemFactory('div');
  dom.p = dom.createElemFactory('p');
  dom.header = dom.createElemFactory('header');
  dom.footer = dom.createElemFactory('footer');
  dom.section = dom.createElemFactory('section');
  dom.span = dom.createElemFactory('span');
  dom.strong = dom.createElemFactory('strong');
  dom.em = dom.createElemFactory('em');
  dom.img = function(src){
    const e = dom.create('img');
    if(src) e.setAttribute('src',src);
    return e;
  };
  /**
     Creates and returns a new anchor element with the given
     optional href and label. If label===true then href is used
     as the label.
  */
  dom.a = function(href,label){
    const e = dom.create('a');
    if(href) e.setAttribute('href',href);
    if(label) e.appendChild(dom.text(true===label ? href : label));
    return e;
  };
  dom.hr = dom.createElemFactory('hr');
  dom.br = dom.createElemFactory('br');
  dom.text = (t)=>document.createTextNode(t||'');
  dom.button = function(label){
    const b = this.create('button');
    if(label) b.appendChild(this.text(label));
    return b;
  };
  dom.select = dom.createElemFactory('select');
  /**
     Returns an OPTION element with the given value and label
     text (which defaults to the value).

     May be called as (value), (selectElement), (selectElement,
     value), (value, label) or (selectElement, value,
     label). The latter appends the new element to the given
     SELECT element.

     If the value has the undefined value then it is NOT
     assigned as the option element's value.
  */
  dom.option = function(value,label){
    const a = arguments;
    var sel;
    if(1==a.length){
      if(a[0] instanceof HTMLElement){
        sel = a[0];
      }else{
        value = a[0];
      }
    }else if(2==a.length){
      if(a[0] instanceof HTMLElement){
        sel = a[0];
        value = a[1];
      }else{
        value = a[0];
        label = a[1];
      }
    }
    else if(3===a.length){
      sel = a[0];
      value = a[1];
      label = a[2];
    }
    const o = this.create('option');
    if(undefined !== value){
      o.value = value;
      this.append(o, this.text(label || value));
    }
    if(sel) this.append(sel, o);
    return o;
  };
  dom.h = function(level){
    return this.create('h'+level);
  };
  dom.ul = dom.createElemFactory('ul');
  /**
     Creates and returns a new LI element, appending it to the
     given parent argument if it is provided.
  */
  dom.li = function(parent){
    const li = this.create('li');
    if(parent) parent.appendChild(li);
    return li;
  };

  /**
     Returns a function which creates a new DOM element of the
     given type and accepts an optional parent DOM element
     argument. If the function's argument is truthy, the new
     child element is appended to the given parent element.
     Returns the new child element.
  */
  dom.createElemFactoryWithOptionalParent = function(childType){
    return function(parent){
      const e = this.create(childType);
      if(parent) parent.appendChild(e);
      return e;
    };
  };
  
  dom.table = dom.createElemFactory('table');
  dom.thead = dom.createElemFactoryWithOptionalParent('thead');
  dom.tbody = dom.createElemFactoryWithOptionalParent('tbody');
  dom.tfoot = dom.createElemFactoryWithOptionalParent('tfoot');
  dom.tr = dom.createElemFactoryWithOptionalParent('tr');
  dom.td = dom.createElemFactoryWithOptionalParent('td');
  dom.th = dom.createElemFactoryWithOptionalParent('th');

  
  /**
     Creates and returns a FIELDSET element, optionaly with a
     LEGEND element added to it.
  */
  dom.fieldset = function(legendText){
    const fs = this.create('fieldset');
    if(legendText){
      this.append(
        fs,
        this.append(
          this.create('legend'),
          legendText
        )
      );
    }
    return fs;
  };

  /**
     Appends each argument after the first to the first argument
     (a DOM node) and returns the first argument.

     - If an argument is a string or number, it is transformed
     into a text node.

     - If an argument is an array or has a forEach member, this
     function appends each element in that list to the target
     by calling its forEach() method to pass it (recursively)
     to this function.

     - Else the argument assumed to be of a type legal
     to pass to parent.appendChild().
  */
  dom.append = function f(parent/*,...*/){
    const a = argsToArray(arguments);
    a.shift();
    for(let i in a) {
      var e = a[i];
      if(isArray(e) || e.forEach){
        e.forEach((x)=>f.call(this, parent,e));
        continue;
      }
      if('string'===typeof e || 'number'===typeof e) e = this.text(e);
      parent.appendChild(e);
    }
    return parent;
  };

  dom.input = function(type){
    return this.attr(this.create('input'), 'type', type);
  };
  
  /**
     Internal impl for addClass(), removeClass().
  */
  const domAddRemoveClass = function f(action,e){
    if(!f.rxSPlus){
      f.rxSPlus = /\s+/;
      f.applyAction = function(e,a,v){
        if(!e || !v
           /*silently skip empty strings/flasy
             values, for user convenience*/) return;
        else if(e.forEach){
          e.forEach((E)=>E.classList[a](v));
        }else{
          e.classList[a](v);
        }
      };
    }
    var i = 2, n = arguments.length;
    for( ; i < n; ++i ){
      let c = arguments[i];
      if(!c) continue;
      else if(isArray(c) ||
              ('string'===typeof c
               && c.indexOf(' ')>=0
               && (c = c.split(f.rxSPlus)))
              || c.forEach
             ){
        c.forEach((k)=>k ? f.applyAction(e, action, k) : false);
        // ^^^ we could arguably call f(action,e,k) to recursively
        // apply constructs like ['foo bar'] or [['foo'],['bar baz']].
      }else if(c){
        f.applyAction(e, action, c);
      }
    }
    return e;
  };

  /**
     Adds one or more CSS classes to one or more DOM elements.

     The first argument is a target DOM element or a list type of such elements
     which has a forEach() method.  Each argument
     after the first may be a string or array of strings. Each
     string may contain spaces, in which case it is treated as a
     list of CSS classes.

     Returns e.
  */
  dom.addClass = function(e,c){
    const a = argsToArray(arguments);
    a.unshift('add');
    return domAddRemoveClass.apply(this, a);
  };
  /**
     The 'remove' counterpart of the addClass() method, taking
     the same arguments and returning the same thing.
  */
  dom.removeClass = function(e,c){
    const a = argsToArray(arguments);
    a.unshift('remove');
    return domAddRemoveClass.apply(this, a);
  };

  dom.hasClass = function(e,c){
    return (e && e.classList) ? e.classList.contains(c) : false;
  };

  /**
     Each argument after the first may be a single DOM element
     or a container of them with a forEach() method. All such
     elements are appended, in the given order, to the dest
     element.

     Returns dest.
  */
  dom.moveTo = function(dest,e){
    const n = arguments.length;
    var i = 1;
    for( ; i < n; ++i ){
      e = arguments[i];
      if(e.forEach){
        e.forEach((x)=>dest.appendChild(x));
      }else{
        dest.appendChild(e);
      }
    }
    return dest;
  };
  /**
     Each argument after the first may be a single DOM element
     or a container of them with a forEach() method. For each
     DOM element argument, all children of that DOM element
     are moved to dest (via appendChild()). For each list argument,
     each entry in the list is assumed to be a DOM element and is
     appended to dest.

     dest may be an Array, in which case each child is pushed
     into the array and removed from its current parent element.

     All children are appended in the given order.

     Returns dest.
  */
  dom.moveChildrenTo = function f(dest,e){
    if(!f.mv){
      f.mv = function(d,v){
        if(d instanceof Array){
          d.push(v);
          if(v.parentNode) v.parentNode.removeChild(v);
        }
        else d.appendChild(v);
      };
    }
    const n = arguments.length;
    var i = 1;
    for( ; i < n; ++i ){
      e = arguments[i];
      if(!e){
        console.warn("Achtung: dom.moveChildrenTo() passed a falsy value at argment",i,"of",
                     arguments,arguments[i]);
        continue;
      }
      if(e.forEach){
        e.forEach((x)=>f.mv(dest, x));
      }else{
        while(e.firstChild){
          f.mv(dest, e.firstChild);
        }
      }
    }
    return dest;
  };

  /**
     Adds each argument (DOM Elements) after the first to the
     DOM immediately before the first argument (in the order
     provided), then removes the first argument from the DOM.
     Returns void.

     If any argument beyond the first has a forEach method, that
     method is used to recursively insert the collection's
     contents before removing the first argument from the DOM.
  */
  dom.replaceNode = function f(old,nu){
    var i = 1, n = arguments.length;
    ++f.counter;
    try {
      for( ; i < n; ++i ){
        const e = arguments[i];
        if(e.forEach){
          e.forEach((x)=>f.call(this,old,e));
          continue;
        }
        old.parentNode.insertBefore(e, old);
      }
    }
    finally{
      --f.counter;
    }
    if(!f.counter){
      old.parentNode.removeChild(old);
    }
  };
  dom.replaceNode.counter = 0;        
  /**
     Two args == getter: (e,key), returns value

     Three == setter: (e,key,val), returns e. If val===null
     or val===undefined then the attribute is removed. If (e)
     has a forEach method then this routine is applied to each
     element of that collection via that method.           
  */
  dom.attr = function f(e){
    if(2===arguments.length) return e.getAttribute(arguments[1]);
    if(e.forEach){
      e.forEach((x)=>f(x,arguments[1],arguments[2]));
      return e;
    }            
    const key = arguments[1], val = arguments[2];
    if(null===val || undefined===val){
      e.removeAttribute(key);
    }else{
      e.setAttribute(key,val);
    }
    return e;
  };

  const enableDisable = function f(enable){
    var i = 1, n = arguments.length;
    for( ; i < n; ++i ){
      let e = arguments[i];
      if(e.forEach){
        e.forEach((x)=>f(enable,x));
        return e;
      }
      e.disabled = !enable;
    }
    return arguments[1];
  };

  /**
     Enables (by removing the "disabled" attribute) each element
     (HTML DOM element or a collection with a forEach method)
     and returns the first argument.
  */
  dom.enable = function(e){
    const args = argsToArray(arguments);
    args.unshift(true);
    return enableDisable.apply(this,args);
  };
  /**
     Disables (by setting the "disabled" attribute) each element
     (HTML DOM element or a collection with a forEach method)
     and returns the first argument.
  */
  dom.disable = function(e){
    const args = argsToArray(arguments);
    args.unshift(false);
    return enableDisable.apply(this,args);
  };

  /**
     A proxy for document.querySelector() which throws if
     selection x is not found. It may optionally be passed an
     "origin" object as its 2nd argument, which restricts the
     search to that branch of the tree.
  */
  dom.selectOne = function(x,origin){
    var src = origin || document,
        e = src.querySelector(x);
    if(!e){
      e = new Error("Cannot find DOM element: "+x);
      console.error(e, src);
      throw e;
    }
    return e;
  };

  return F.dom = dom;
})(window.fossil);
Changes to src/fossil.fetch.js.
36
37
38
39
40
41
42
43

44
45
46
47
48
49
50
51
52
53
54
  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)){

      if('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);
  }







|
>
|
|
|
<







36
37
38
39
40
41
42
43
44
45
46
47

48
49
50
51
52
53
54
  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);
  }
Changes to src/fossil.page.fileedit.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
(function(){
  "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);

  window.addEventListener("load", function() {
    const P = fossil.page;

    P.e = {
      taEditor: E('#fileedit-content'),
      taComment: E('#fileedit-comment'),
      ajaxContentTarget: E('#ajax-target'),
      form: E('#fileedit-form'),
      btnPreview: E("#fileedit-btn-preview"),
      btnDiffSbs: E("#fileedit-btn-diffsbs"),
      btnDiffU: E("#fileedit-btn-diffu"),
      btnCommit: E("#fileedit-btn-commit"),
      selectPreviewModeWrap: E('#select-preview-mode'),
      selectHtmlEmsWrap: E('#select-preview-html-ems'),
      selectEolWrap:  E('#select-preview-html-ems'),
      cbLineNumbersWrap: E('#cb-line-numbers')






    };
    const stopEvent = function(e){
      e.preventDefault();
      e.stopPropagation();
      return P;
    };
      
    P.e.form.addEventListener("submit", function(e) {
      e.target.checkValidity();
      stopEvent(e);


    }, false);



    P.e.btnPreview.addEventListener(
      "click",(e)=>stopEvent(e).preview(),false
    );

    P.e.btnDiffSbs.addEventListener(

      "click",(e)=>stopEvent(e).diff(true),false


    );


    P.e.btnDiffU.addEventListener(



      "click",(e)=>stopEvent(e).diff(false), false
    );
    P.e.btnCommit.addEventListener(
      "click",(e)=>stopEvent(e).commit(), false
    );

    /**
       Cosmetic: jump through some hoops to enable/disable
       certain preview options depending on the current
       preview mode...
    */
    const selectPreviewMode =
          P.e.selectPreviewModeWrap.querySelector('select');
    selectPreviewMode.addEventListener(
      "change",function(e){
        const mode = e.target.value,
              name = P.previewModes[mode],
              hide = [], unhide = [];
        if('guess'===name){
          unhide.push(P.e.cbLineNumbersWrap,
                      P.e.selectHtmlEmsWrap);
        }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
(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;
  window.addEventListener("load", function() {
    const P = F.page;
    P.tabs = new fossil.TabManager('#fileedit-tabs');
    P.e = {
      taEditor: E('#fileedit-content-editor'),
      taComment: E('#fileedit-comment'),
      ajaxContentTarget: E('#ajax-target'),
      form: E('#fileedit-form'),
      //btnPreview: E("#fileedit-btn-preview"),
      //btnDiffSbs: E("#fileedit-btn-diffsbs"),
      //btnDiffU: E("#fileedit-btn-diffu"),
      btnCommit: E("#fileedit-btn-commit"),
      selectPreviewModeWrap: E('#select-preview-mode'),
      selectHtmlEmsWrap: E('#select-preview-html-ems'),
      selectEolWrap:  E('#select-preview-html-ems'),
      cbLineNumbersWrap: E('#cb-line-numbers'),
      tabs:{
        content: E('#fileedit-tab-content'),
        preview: E('#fileedit-tab-preview'),
        diff: E('#fileedit-tab-diff'),
        commit: E('#fileedit-tab-commit')
      }
    };
    const stopEvent = function(e){
      //e.preventDefault();
      //e.stopPropagation();
      return P;
    };
      
    P.e.form.addEventListener("submit", function(e) {
      e.target.checkValidity();
      e.preventDefault();
      e.stopPropagation();
      return false;
    }, false);
    //P.tabs.getButtonForTab(P.e.tabs.preview)
    P.e.tabs.preview.querySelector(
      'button'
    ).addEventListener(
      "click",(e)=>P.preview(), false
    );

    document.querySelector('#fileedit-form').addEventListener(
      "click",function(e){
        stopEvent(e);
        return false;
      }
    );
    
    const diffButtons = E('#fileedit-tab-diff-buttons');
    diffButtons.querySelector('button.sbs').addEventListener(
      "click",(e)=>P.diff(true), false
    );
    diffButtons.querySelector('button.unified').addEventListener(
      "click",(e)=>P.diff(false), false
    );
    P.e.btnCommit.addEventListener(
      "click",(e)=>stopEvent(e).commit(), false
    );

    /**
       Cosmetic: jump through some hoops to enable/disable
       certain preview options depending on the current
       preview mode...
    */
    const selectPreviewMode =
          P.e.selectPreviewModeWrap.querySelector('select');
    selectPreviewMode.addEventListener(
      "change", function(e){
        const mode = e.target.value,
              name = P.previewModes[mode],
              hide = [], unhide = [];
        if('guess'===name){
          unhide.push(P.e.cbLineNumbersWrap,
                      P.e.selectHtmlEmsWrap);
        }else{
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
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
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
214
215
216
217
218
219









220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
        P.e.taEditor.classList.add('font-size-'+e.target.value);
      }, false
    );
    selectFontSize.dispatchEvent(
      // Force UI update
      new Event('change',{target:selectFontSize})
    );
  }, false);





  
  /**
     updateVersion() updates filename and version in relevant UI
     elements...

     Returns this object.
  */
  fossil.page.updateVersion = function(file,rev){
    this.finfo = {file,r:rev};
    const E = (s)=>document.querySelector(s),
          euc = encodeURIComponent;

    E('#r-label').innerText=rev;
    E('#finfo-link').setAttribute(
      'href',
      fossil.rootPath+'finfo?name='+euc(file)+'&m='+rev
    );
    E('#finfo-file-name').innerText=file;
    E('#r-link').setAttribute(
      'href',
      fossil.rootPath+'/info/'+rev
    );
    E('#r-label').innerText = rev;
    const purl = fossil.rootPath+'fileedit?file='+euc(file)+
          '&r='+rev;
    var e = E('#permalink');
    e.innerText=purl;
    e.setAttribute('href',purl);
    return this;
  };

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

     Returns this object, noting that the load is async.
  */
  fossil.page.loadFile = function(file,rev){
    delete this.finfo;

    fossil.fetch('fileedit_content',{
      urlParams:{file:file,r:rev},
      onload:(r)=>{
        document.getElementById('fileedit-content').value=r;
        fossil.message('Loaded content.');

        fossil.page.updateVersion(file,rev);


      }
    });
    return this;
  };

  /**
     Fetches the page preview based on the contents and settings of this
     page's form, and updates this.e.ajaxContentTarget with the preview.

     Returns this object, noting that the operation is async.
  */
  fossil.page.preview = function(){
    if(!this.finfo){
      fossil.error("No content is loaded.");
      return this;
    }
    const content = this.e.taEditor.value,
          target = this.e.ajaxContentTarget;



    const updateView = function(c){
      target.innerHTML = [
        "<div class='fileedit-preview'>",
        "<div>Preview</div>",
        c||'',
        "</div><!--.fileedit-diff-->"
      ].join('');
      fossil.message('Updated preview.');

    };
    if(!content){
      updateView('');
      return this;
    }
    const fd = new FormData();
    fd.append('render_mode',E('select[name=preview_render_mode]').value);
    fd.append('file',this.finfo.file);
    fd.append('ln',E('[name=preview_ln]').checked ? 1 : 0);
    fd.append('iframe_height', E('[name=preview_html_ems]').value);
    fd.append('content',content);

    fossil.message(
      "Fetching preview..."
    ).fetch('fileedit_preview',{
      payload: fd,
      onload: updateView
    });
    return this;
  };

  /**
     Fetches the page preview based on the contents and settings of this
     page's form, and updates this.e.ajaxContentTarget with the preview.

     Returns this object, noting that the operation is async.
  */
  fossil.page.diff = function(sbs){
    if(!this.finfo){
      fossil.error("No content is loaded.");
      return this;
    }
    const self = this;
    const content = this.e.taEditor.value,
          target = this.e.ajaxContentTarget;
    const updateView = function(c){
      target.innerHTML = [
        "<div class='fileedit-diff'>",
        "<div>Diff <code>[",
        self.finfo.r,
        "]</code> &rarr; Local Edits</div>",
        c||'',
        "</div><!--.fileedit-diff-->"
      ].join('');
      fossil.message('Updated diff.');
    };
    if(!content){
      updateView('');
      return this;
    }
    const fd = new FormData();
    fd.append('file',this.finfo.file);
    fd.append('r', this.finfo.r);
    fd.append('sbs', sbs ? 1 : 0);
    fd.append('content',content);
    fossil.message(
      "Fetching diff..."
    ).fetch('fileedit_diff',{
      payload: fd,
      onload: updateView









    });
    return this;
  };

  /**
     Performs an async commit based on the form contents and updates
     the UI.

     Returns this object.
  */
  fossil.page.commit = function f(){
    if(!this.finfo){
      fossil.error("No content is loaded.");
      return this;
    }
    const self = this;
    const content = this.e.taEditor.value,
          target = this.e.ajaxContentTarget,
          cbDryRun = E('[name=dry_run]'),
          isDryRun = cbDryRun.checked,
          filename = this.finfo.file;
    if(!f.updateView){
      f.updateView = function(c){
        target.innerHTML = [
          "<h3>Manifest",







<

|
>
>
>







|


|
>



|




|


|
|
|
|










|

>
|


<
|
>
|
>
>











|

|



|
>
>
>

|
<
<
<
|
<
|
>











>
|














|

|


<

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





|



|
>
>
>
>
>
>
>
>
>










|

|




|







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
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
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
214
215
216
217
218
219

220
221







222
223




224

225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
        P.e.taEditor.classList.add('font-size-'+e.target.value);
      }, false
    );
    selectFontSize.dispatchEvent(
      // Force UI update
      new Event('change',{target:selectFontSize})
    );


    P.tabs.e.container.insertBefore(
      E('#fossil-status-bar'), P.tabs.e.tabs
    );
  }, false);
  
  /**
     updateVersion() updates filename and version in relevant UI
     elements...

     Returns this object.
  */
  F.page.updateVersion = function(file,rev){
    this.finfo = {file,r:rev};
    const E = (s)=>document.querySelector(s),
          euc = encodeURIComponent,
          rShort = rev.substr(0,16);
    E('#r-label').innerText=rev;
    E('#finfo-link').setAttribute(
      'href',
      F.repoUrl('finfo',{name:file, m:rShort})
    );
    E('#finfo-file-name').innerText=file;
    E('#r-link').setAttribute(
      'href',
      F.repoUrl('info/'+rev)
    );
    E('#r-label').innerText = rev;
    const purlArgs = F.encodeUrlArgs({file, r:rShort});
    const purl = F.repoUrl('fileedit',purlArgs);
    const e = E('#permalink');
    e.innerText='fileedit?'+purlArgs;
    e.setAttribute('href',purl);
    return this;
  };

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

     Returns this object, noting that the load is async.
  */
  F.page.loadFile = function(file,rev){
    delete this.finfo;
    const self = this;
    F.fetch('fileedit_content',{
      urlParams:{file:file,r:rev},
      onload:(r)=>{

        F.message('Loaded content.');
        self.e.taEditor.value = r;
        self.updateVersion(file,rev);
        self.preview();
        self.tabs.switchToTab(self.e.tabs.content);
      }
    });
    return this;
  };

  /**
     Fetches the page preview based on the contents and settings of this
     page's form, and updates this.e.ajaxContentTarget with the preview.

     Returns this object, noting that the operation is async.
  */
  F.page.preview = function(switchToTab){
    if(!this.finfo){
      F.error("No content is loaded.");
      return this;
    }
    const content = this.e.taEditor.value,
          target = this.e.tabs.preview.querySelector(
            '#fileedit-tab-preview-wrapper'
          ),
          self = this;
    const updateView = function(c){
      if(c) target.innerHTML = c;



      else D.clearElement(target);

      F.message('Updated preview.');
      if(switchToTab) self.tabs.switchToTab(self.e.tabs.preview);
    };
    if(!content){
      updateView('');
      return this;
    }
    const fd = new FormData();
    fd.append('render_mode',E('select[name=preview_render_mode]').value);
    fd.append('file',this.finfo.file);
    fd.append('ln',E('[name=preview_ln]').checked ? 1 : 0);
    fd.append('iframe_height', E('[name=preview_html_ems]').value);
    fd.append('content',content);
    target.innerText = "Fetching preview...";
    F.message(
      "Fetching preview..."
    ).fetch('fileedit_preview',{
      payload: fd,
      onload: updateView
    });
    return this;
  };

  /**
     Fetches the page preview based on the contents and settings of this
     page's form, and updates this.e.ajaxContentTarget with the preview.

     Returns this object, noting that the operation is async.
  */
  F.page.diff = function(sbs){
    if(!this.finfo){
      F.error("No content is loaded.");
      return this;
    }

    const content = this.e.taEditor.value,
          target = this.e.tabs.diff.querySelector(







            '#fileedit-tab-diff-wrapper'
          ),




          self = this;

    const fd = new FormData();
    fd.append('file',this.finfo.file);
    fd.append('r', this.finfo.r);
    fd.append('sbs', sbs ? 1 : 0);
    fd.append('content',content);
    F.message(
      "Fetching diff..."
    ).fetch('fileedit_diff',{
      payload: fd,
      onload: function(c){
        target.innerHTML = [
          "<div>Diff <code>[",
          self.finfo.r,
          "]</code> &rarr; Local Edits</div>",
          c||'No changes.'
        ].join('');
        F.message('Updated diff.');
        self.tabs.switchToTab(self.e.tabs.diff);
      }
    });
    return this;
  };

  /**
     Performs an async commit based on the form contents and updates
     the UI.

     Returns this object.
  */
  F.page.commit = function f(){
    if(!this.finfo){
      F.error("No content is loaded.");
      return this;
    }
    const self = this;
    const content = this.e.taEditor.value,
          target = document.querySelector('#fileedit-manifest'),
          cbDryRun = E('[name=dry_run]'),
          isDryRun = cbDryRun.checked,
          filename = this.finfo.file;
    if(!f.updateView){
      f.updateView = function(c){
        target.innerHTML = [
          "<h3>Manifest",
253
254
255
256
257
258
259
260
261
262

263
264
265
266
267
268
269
          c.dryRun ? '(dry run)' : '',
          '[', c.uuid,'].'
        ];
        if(!c.dryRun){
          msg.push('Re-activating dry-run mode.');
          self.e.taComment.value = '';
          cbDryRun.checked = true;
          fossil.page.updateVersion(filename, c.uuid);
        }
        fossil.message.apply(fossil, msg);

      };
    }
    if(!content){
      f.updateView('');
      return this;
    }
    const fd = new FormData();







|

|
>







277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
          c.dryRun ? '(dry run)' : '',
          '[', c.uuid,'].'
        ];
        if(!c.dryRun){
          msg.push('Re-activating dry-run mode.');
          self.e.taComment.value = '';
          cbDryRun.checked = true;
          F.page.updateVersion(filename, c.uuid);
        }
        F.message.apply(fossil, msg);
        self.tabs.switchToTab(self.e.tabs.commit);
      };
    }
    if(!content){
      f.updateView('');
      return this;
    }
    const fd = new FormData();
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
      var e = E('[name='+name+']');
      if(e){
        if(e.checked) fd.append(name, 1);
      }else{
        console.error("Missing checkbox? name =",name);
      }
    });
    fossil.message(
      "Checking in..."
    ).fetch('fileedit_commit',{
      payload: fd,
      responseType: 'json',
      onload: f.updateView
    });
    return this;
  };

  
})();







|









|
<
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330

      var e = E('[name='+name+']');
      if(e){
        if(e.checked) fd.append(name, 1);
      }else{
        console.error("Missing checkbox? name =",name);
      }
    });
    F.message(
      "Checking in..."
    ).fetch('fileedit_commit',{
      payload: fd,
      responseType: 'json',
      onload: f.updateView
    });
    return this;
  };

})(window.fossil);

Added 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
107
108
109
110
111
112
113
114
115
116
"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{
          setVisible(e, false);
          D.removeClass(btn,'selected');
        }
      });
      return this;
    }
  };

  F.TabManager = TabManager;
})(window.fossil);
Changes to src/main.mk.
217
218
219
220
221
222
223

224
225

226
227
228
229
230
231
232
  $(SRCDIR)/../skins/xekri/header.txt \
  $(SRCDIR)/accordion.js \
  $(SRCDIR)/ci_edit.js \
  $(SRCDIR)/copybtn.js \
  $(SRCDIR)/diff.tcl \
  $(SRCDIR)/forum.js \
  $(SRCDIR)/fossil.bootstrap.js \

  $(SRCDIR)/fossil.fetch.js \
  $(SRCDIR)/fossil.page.fileedit.js \

  $(SRCDIR)/graph.js \
  $(SRCDIR)/href.js \
  $(SRCDIR)/login.js \
  $(SRCDIR)/markdown.md \
  $(SRCDIR)/menu.js \
  $(SRCDIR)/sbsdiff.js \
  $(SRCDIR)/scroll.js \







>


>







217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
  $(SRCDIR)/../skins/xekri/header.txt \
  $(SRCDIR)/accordion.js \
  $(SRCDIR)/ci_edit.js \
  $(SRCDIR)/copybtn.js \
  $(SRCDIR)/diff.tcl \
  $(SRCDIR)/forum.js \
  $(SRCDIR)/fossil.bootstrap.js \
  $(SRCDIR)/fossil.dom.js \
  $(SRCDIR)/fossil.fetch.js \
  $(SRCDIR)/fossil.page.fileedit.js \
  $(SRCDIR)/fossil.tabs.js \
  $(SRCDIR)/graph.js \
  $(SRCDIR)/href.js \
  $(SRCDIR)/login.js \
  $(SRCDIR)/markdown.md \
  $(SRCDIR)/menu.js \
  $(SRCDIR)/sbsdiff.js \
  $(SRCDIR)/scroll.js \
Changes to src/style.c.
1420
1421
1422
1423
1424
1425
1426































1427


1428
1429
1430

1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464









1465

1466
1467

1468

1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
  CX("</select>\n");
  if(zLabel && *zLabel){
    CX("</span>\n");
  }
  va_end(vargs);
}



































/*
** If passed 0, it emits a script opener tag with this request's
** nonce. If passed non-0 it emits a script closing tag. The very

** first time it is called, it emits some bootstrapping JS code
** immediately after the script opener. Specifically, it defines
** window.fossil if it's not already defined, and may set some
** properties on it.
*/
void style_emit_script_tag(int phase){
  static int once = 0;
  if(0==phase){
    CX("<script nonce='%s'>", style_nonce());
    if(0==once){
      once = 1;
      /* Set up the generic/app-agnostic parts of window.fossil */
      CX("(function(){\n");
      CX("\nif(!window.fossil) window.fossil={};\n");
      CX("window.fossil.version = '%j';\n", get_version());
      /* fossil.rootPath is the top-most CGI/server path,
         including a trailing slash. */
      CX("window.fossil.rootPath = '%j'+'/';\n", g.zTop);
      /*
      ** fossil.page holds info about the current page. This is
      ** also where the current page "should" store any of its
      ** own page-specific state.
      */
      CX("window.fossil.page = {"
         "page:'%T'"
         "};\n", g.zPath);
      CX("%s\n", builtin_text("fossil.bootstrap.js"));
      CX("})();\n");
    }
  }else{
    CX("</script>\n");
  }
}












/*
** The *FIRST* time this is called, it emits a JS script block,

** including tags, which defines window.fossil.fetch(), which works

** similarly (not identically) to the not-quite-ubiquitous global
** fetch(). It calls style_emit_script_tag(), which may inject
** other JS bootstrapping bits.
**
** JS usages:
**
** fossil.fetch( URI [, onLoadCallback] );
**
** fossil.fetch( URI [, optionsObject = {}] );
**







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


|
>
|
|
|
|





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






>
>
>
>
>
>
>
>
>
|
>

|
>
|
>
|
|
|







1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
















1475

1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
  CX("</select>\n");
  if(zLabel && *zLabel){
    CX("</span>\n");
  }
  va_end(vargs);
}

/*
** The first time this is called, it emits code to install and
** bootstrap the window.fossil object, using the built-in file
** fossil.bootstrap.js (not to be confused with bootstrap.js). It does
** NOT wrap that in a script tag because it's called from
** style_emit_script_tag().
**
** Subsequent calls are no-ops.
*/
static void style_emit_script_fossil_bootstrap(){
  static int once = 0;
  if(0==once++){
    /* Set up the generic/app-agnostic parts of window.fossil */
    CX("(function(){\n"
       "if(!window.fossil) window.fossil={};\n"
       "window.fossil.version = \"%j\";\n"
    /* fossil.rootPath is the top-most CGI/server path,
       including a trailing slash. */
       "window.fossil.rootPath = \"%j\"+'/';\n",
       get_version(), g.zTop);
    /*
    ** fossil.page holds info about the current page. This is
    ** also where the current page "should" store any of its
    ** own page-specific state.
    */
    CX("window.fossil.page = {"
       "page:\"%T\""
       "};\n", g.zPath);
    /* The remaining code is not dependent on C-runtime state... */
    CX("%s\n", builtin_text("fossil.bootstrap.js"));
    CX("})();\n");
  }
}

/*
** If passed 0, it emits a script opener tag with this request's
** nonce. If passed non-0 it emits a script closing tag.
**
** The very first time it is called, it emits some bootstrapping JS
** code immediately after the script opener. Specifically, it defines
** window.fossil if it's not already defined, and sets up its most
** basic functionality.
*/
void style_emit_script_tag(int phase){
  static int once = 0;
  if(0==phase){
    CX("<script nonce='%s'>", style_nonce());
    if(0==once++){
















      style_emit_script_fossil_bootstrap();

    }
  }else{
    CX("</script>\n");
  }
}

/*
** Emits the text of builtin_text(zName), which is assumed to be
** JavaScript code, and wrapps that in a pair of calls to
** style_emit_script_tag().
*/
void style_emit_script_builtin(char const * zName){
    style_emit_script_tag(0);
    CX("%s", builtin_text(zName));
    style_emit_script_tag(1);
}

/*
** The first time this is called, it emits a JS script block,
** including tags, using the contents of the built-in file
** fossil.fetch.js, which defines window.fossil.fetch(), an HTTP
** request/response mini-framework similar (but not identical) to the
** not-quite-ubiquitous window.fetch(). It calls
** style_emit_script_tag(), which may inject other JS bootstrapping
** bits. Subsequent calls are no-ops.
**
** JS usages:
**
** fossil.fetch( URI [, onLoadCallback] );
**
** fossil.fetch( URI [, optionsObject = {}] );
**
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531









1532

1533


1534








1535

1536
1537
** - 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 set then
**   the method is automatically set to 'POST'. If an object/array is
**   converted to JSON, the content-type is set to 'application/json'.
**   By default XHR2 will set the content type based on the payload
**   type.
**
** - contentType: Optional request content type when POSTing. Ignored
**   if the method is not 'POST'.
**
** - responseType: optional string. One of ("text", "arraybuffer",
**   "blob", or "document") (as specified by XHR2). Default = "text".
**   As an extension, it supports "json", which tells it that the
**   response is expected to be text and that it should be
**   JSON.parse()d before passing it on to the onload() callback. In
**   this case, if the payload property is an object/array.
**
** - urlParams: string|object. If a string, it is assumed to be a
**   URI-encoded list of params in the form "key1=val1&key2=val2...",
**   with NO leading '?'.  If it is an object, all of its properties
**   get converted to that form. Either way, the parameters get
**   appended to the URL.
**
** When an options object does not provide onload() or onerror()
** handlers of its own, this function falls back to
** fossil.fetch.onload() and fossil.fetch.onerror() as defaults. The
** default implementations route the data through the dev console and
** (for onerror()) through fossil.error(). Individual pages may
** 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.
*/
void style_emit_script_fetch(){
  static int once = 0;
  if(0==once){









    once = 1;

    style_emit_script_tag(0);


    CX("%s", builtin_text("fossil.fetch.js"));








    style_emit_script_tag(1);

  }
}







|
|
|
|
|








|
<





|














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


1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538

1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
** - 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 based on the payload type.
**
** - contentType: Optional request content type when POSTing. Ignored
**   if the method is not 'POST'.
**
** - responseType: optional string. One of ("text", "arraybuffer",
**   "blob", or "document") (as specified by XHR2). Default = "text".
**   As an extension, it supports "json", which tells it that the
**   response is expected to be text and that it should be
**   JSON.parse()d before passing it on to the onload() callback.

**
** - urlParams: string|object. If a string, it is assumed to be a
**   URI-encoded list of params in the form "key1=val1&key2=val2...",
**   with NO leading '?'.  If it is an object, all of its properties
**   get converted to that form. Either way, the parameters get
**   appended to the URL before submitting the request.
**
** When an options object does not provide onload() or onerror()
** handlers of its own, this function falls back to
** fossil.fetch.onload() and fossil.fetch.onerror() as defaults. The
** default implementations route the data through the dev console and
** (for onerror()) through fossil.error(). Individual pages may
** 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.
*/
void style_emit_script_fetch(){
  static int once = 0;
  if(0==once++){
    style_emit_script_builtin("fossil.fetch.js");
  }
}

/*
** The first time this is called, it emits the JS code from the
** built-in file fossil.dom.js. Subsequent calls are no-ops.
*/
void style_emit_script_dom(){
  static int once = 0;
  if(0==once++){
    style_emit_script_builtin("fossil.dom.js");
  }
}

/*
** The first time this is called, it calls style_emit_script_dom() and
** emits the JS code from the built-in file fossil.tabs.js.
** Subsequent calls are no-ops.
*/
void style_emit_script_tabs(){
  static int once = 0;
  if(0==once++){
    style_emit_script_dom();
    style_emit_script_builtin("fossil.tabs.js");
  }
}
Changes to win/Makefile.mingw.
639
640
641
642
643
644
645

646
647

648
649
650
651
652
653
654
  $(SRCDIR)/../skins/xekri/header.txt \
  $(SRCDIR)/accordion.js \
  $(SRCDIR)/ci_edit.js \
  $(SRCDIR)/copybtn.js \
  $(SRCDIR)/diff.tcl \
  $(SRCDIR)/forum.js \
  $(SRCDIR)/fossil.bootstrap.js \

  $(SRCDIR)/fossil.fetch.js \
  $(SRCDIR)/fossil.page.fileedit.js \

  $(SRCDIR)/graph.js \
  $(SRCDIR)/href.js \
  $(SRCDIR)/login.js \
  $(SRCDIR)/markdown.md \
  $(SRCDIR)/menu.js \
  $(SRCDIR)/sbsdiff.js \
  $(SRCDIR)/scroll.js \







>


>







639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
  $(SRCDIR)/../skins/xekri/header.txt \
  $(SRCDIR)/accordion.js \
  $(SRCDIR)/ci_edit.js \
  $(SRCDIR)/copybtn.js \
  $(SRCDIR)/diff.tcl \
  $(SRCDIR)/forum.js \
  $(SRCDIR)/fossil.bootstrap.js \
  $(SRCDIR)/fossil.dom.js \
  $(SRCDIR)/fossil.fetch.js \
  $(SRCDIR)/fossil.page.fileedit.js \
  $(SRCDIR)/fossil.tabs.js \
  $(SRCDIR)/graph.js \
  $(SRCDIR)/href.js \
  $(SRCDIR)/login.js \
  $(SRCDIR)/markdown.md \
  $(SRCDIR)/menu.js \
  $(SRCDIR)/sbsdiff.js \
  $(SRCDIR)/scroll.js \
Changes to win/Makefile.msc.
546
547
548
549
550
551
552

553
554

555
556
557
558
559
560
561
        $(SRCDIR)\..\skins\xekri\header.txt \
        $(SRCDIR)\accordion.js \
        $(SRCDIR)\ci_edit.js \
        $(SRCDIR)\copybtn.js \
        $(SRCDIR)\diff.tcl \
        $(SRCDIR)\forum.js \
        $(SRCDIR)\fossil.bootstrap.js \

        $(SRCDIR)\fossil.fetch.js \
        $(SRCDIR)\fossil.page.fileedit.js \

        $(SRCDIR)\graph.js \
        $(SRCDIR)\href.js \
        $(SRCDIR)\login.js \
        $(SRCDIR)\markdown.md \
        $(SRCDIR)\menu.js \
        $(SRCDIR)\sbsdiff.js \
        $(SRCDIR)\scroll.js \







>


>







546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
        $(SRCDIR)\..\skins\xekri\header.txt \
        $(SRCDIR)\accordion.js \
        $(SRCDIR)\ci_edit.js \
        $(SRCDIR)\copybtn.js \
        $(SRCDIR)\diff.tcl \
        $(SRCDIR)\forum.js \
        $(SRCDIR)\fossil.bootstrap.js \
        $(SRCDIR)\fossil.dom.js \
        $(SRCDIR)\fossil.fetch.js \
        $(SRCDIR)\fossil.page.fileedit.js \
        $(SRCDIR)\fossil.tabs.js \
        $(SRCDIR)\graph.js \
        $(SRCDIR)\href.js \
        $(SRCDIR)\login.js \
        $(SRCDIR)\markdown.md \
        $(SRCDIR)\menu.js \
        $(SRCDIR)\sbsdiff.js \
        $(SRCDIR)\scroll.js \