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: |
33ffe5762bef215a33c802bedd28a0ef |
| 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
Changes to src/default_css.txt.
| ︙ | ︙ | |||
864 865 866 867 868 869 870 |
// }
// .fileedit-XXX => /fileedit page
form.fileedit textarea {
font-family: monospace;
width: 100%;
}
form.fileedit fieldset {
| | > | 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 |
overflow: auto;
white-space: pre;
}
div.fileedit-preview {
margin: 0;
padding: 0;
}
| < < < < | | | > > > | 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 |
vertical-align: sub;
}
.input-with-label > span {
margin: 0 0.25em 0 0.25em;
vertical-align: middle;
}
.hidden {
| > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 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 |
}
/*
** Emits utility script code specific to the /fileedit page.
*/
static void fileedit_emit_page_script(){
| | < | > | 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 | ** 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. ********************************************************************/ | < < < < < < < < < < < < < < < < < < | > > > > > > > > > > | | < < < < < < < < < | | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | > | | | | | | | | < < < < < < < < < < < < < < < | | < < < < < < < < < < < < < < < < < < < < | | | | > > > > > | < < > | < < < < | > > | < < | > > > > < < < < < < < < < < < < < < < < < | | < < < < | 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 |
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){
| > > > > > > > > > > > > > > > > > > > > > > < | < < < | 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 |
let payload = opt.payload, jsonResponse = false;
if(payload){
opt.method = 'POST';
if(!(payload instanceof FormData)
&& !(payload instanceof Document)
&& !(payload instanceof Blob)
&& !(payload instanceof File)
| | > | | | < | 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 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 |
P.e.taEditor.classList.add('font-size-'+e.target.value);
}, false
);
selectFontSize.dispatchEvent(
// Force UI update
new Event('change',{target:selectFontSize})
);
| < | > > > | | > | | | | | | | > | < | > | > > | | | > > > | < < < | < | > > | | | < | < < < < < < < | | < < < < | < | | > > > > > > > > > | | | | 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> → 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 |
c.dryRun ? '(dry run)' : '',
'[', c.uuid,'].'
];
if(!c.dryRun){
msg.push('Re-activating dry-run mode.');
self.e.taComment.value = '';
cbDryRun.checked = true;
| | | > | 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 |
var e = E('[name='+name+']');
if(e){
if(e.checked) fd.append(name, 1);
}else{
console.error("Missing checkbox? name =",name);
}
});
| | | < | 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 |
CX("</select>\n");
if(zLabel && *zLabel){
CX("</span>\n");
}
va_end(vargs);
}
| > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | > > | > | | | | | < < < < < < < < < < < < < < < < | < > > > > > > > > > | > | > | > | | | | 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 | ** - 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 | | | | | | | < | | > > > > > > > > > | > | > > | > > > > > > > > | > | 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 \
|
| ︙ | ︙ |