Many hyperlinks are disabled.
Use anonymous login
to enable hyperlinks.
Overview
| Comment: | Initial work on ajaxifying /fileedit. Fetching content, preview, and diffs are ajax'd, but save is not yet. |
|---|---|
| Downloads: | Tarball | ZIP archive |
| Timelines: | family | ancestors | descendants | both | fileedit-ajaxify |
| Files: | files | file ages | folders |
| SHA3-256: |
8edf9dbfc2858f106d5b08d0fb51c38d |
| User & Date: | stephan 2020-05-05 04:06:01.556 |
Context
|
2020-05-05
| ||
| 06:48 | Ajaxified commit. All that's left is cleanup and prettification. ... (check-in: 1a6c5090ce user: stephan tags: fileedit-ajaxify) | |
| 04:06 | Initial work on ajaxifying /fileedit. Fetching content, preview, and diffs are ajax'd, but save is not yet. ... (check-in: 8edf9dbfc2 user: stephan tags: fileedit-ajaxify) | |
|
2020-05-04
| ||
| 23:26 | Moved some generic fileedit code to style.c. Refactored /fileedit to not require JS to update version info, making this impl pure no-JS. Now to ajaxify it... ... (Closed-Leaf check-in: 8d4ce834ed user: stephan tags: checkin-without-checkout) | |
Changes
Changes to src/default_css.txt.
| ︙ | ︙ | |||
916 917 918 919 920 921 922 |
div.fileedit-diff {
margin: 0;
padding: 0;
}
.fileedit-diff > div:first-child {
border-bottom: 1px dashed;
}
| > > > > > > > > > | > > > > | 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 |
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;
border-radius: 0.5em;
padding: 0.25em 0.4em;
margin: 0 0.5em;
display: inline-block;
cursor: pointer;
|
| ︙ | ︙ |
Changes to src/fileedit.c.
| ︙ | ︙ | |||
896 897 898 899 900 901 902 |
if(0==zGlobs) return 0;
pGlobs = glob_create(zGlobs);
fossil_free(zGlobs);
}
return glob_match(pGlobs, zFilename);
}
| < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < > | 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 |
if(0==zGlobs) return 0;
pGlobs = glob_create(zGlobs);
fossil_free(zGlobs);
}
return glob_match(pGlobs, zFilename);
}
enum fileedit_render_preview_flags {
FE_PREVIEW_LINE_NUMBERS = 1
};
enum fileedit_render_modes {
/* GUESS must be 0. All others have specified values. */
FE_RENDER_GUESS = 0,
FE_RENDER_PLAIN_TEXT,
FE_RENDER_HTML,
FE_RENDER_WIKI
};
static int fileedit_render_mode_for_mimetype(const char * zMimetype){
|
| ︙ | ︙ | |||
1035 1036 1037 1038 1039 1040 1041 |
int flags, int renderMode,
int nIframeHeightEm){
const char * zMime;
zMime = mimetype_from_name(zFilename);
if(FE_RENDER_GUESS==renderMode){
renderMode = fileedit_render_mode_for_mimetype(zMime);
}
| < < | 933 934 935 936 937 938 939 940 941 942 943 944 945 946 |
int flags, int renderMode,
int nIframeHeightEm){
const char * zMime;
zMime = mimetype_from_name(zFilename);
if(FE_RENDER_GUESS==renderMode){
renderMode = fileedit_render_mode_for_mimetype(zMime);
}
switch(renderMode){
case FE_RENDER_HTML:{
char * z64 = encode64(blob_str(pContent), blob_size(pContent));
CX("<iframe width='100%%' frameborder='0' "
"marginwidth='0' style='height:%dem' "
"marginheight='0' sandbox='allow-same-origin' "
"id='ifm1' src='data:text/html;base64,%z'"
|
| ︙ | ︙ | |||
1066 1067 1068 1069 1070 1071 1072 |
zExt+1, zContent);
}else{
CX("<pre>%h</pre>", zExt+1, zContent);
}
break;
}
}
| < < < < < | 962 963 964 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 |
zExt+1, zContent);
}else{
CX("<pre>%h</pre>", zExt+1, zContent);
}
break;
}
}
}
/*
** Renders diffs for the /fileedit page. pContent is the
** locally-edited content. frid is the RID of the file's blob entry
** from which pContent is based. zManifestUuid is the checkin version
** to which RID belongs - it is purely informational, for labeling the
** diff view. isSbs is true for side-by-side diffs, false for unified.
*/
static void fileedit_render_diff(Blob * pContent, int frid,
const char * zManifestUuid,
int isSbs){
Blob orig = empty_blob;
Blob out = empty_blob;
u64 diffFlags = DIFF_HTML | DIFF_NOTTOOBIG | DIFF_STRIP_EOLCR
| (isSbs ? DIFF_SIDEBYSIDE : DIFF_LINENO);
content_get(frid, &orig);
text_diff(&orig, pContent, &out, 0, diffFlags);
if(isSbs){
CX("%b",&out);
}else{
CX("<pre class='udiff'>%b</pre>",&out);
}
blob_reset(&orig);
blob_reset(&out);
}
/*
** Given a repo-relative filename and a manifest RID, returns the UUID
** of the corresponding file entry. Returns NULL if no match is
|
| ︙ | ︙ | |||
1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 |
if(pFilePerm){
*pFilePerm = mfile_permstr_int(db_column_text(&stmt, 1));
}
}
db_finalize(&stmt);
return zFileUuid;
}
/*
** WEBPAGE: fileedit
**
** EXPERIMENTAL and subject to change and removal at any time. The goal
** is to allow online edits of files.
**
| > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 |
if(pFilePerm){
*pFilePerm = mfile_permstr_int(db_column_text(&stmt, 1));
}
}
db_finalize(&stmt);
return zFileUuid;
}
/*
** Helper for /filepage_xyz routes. Clears the CGI content buffer,
** sets an error status code, and queues up a JSON response in the
** form of an object:
**
** {error: formatted message}
**
** After calling this, the caller should immediately return.
*/
static void fileedit_ajax_error(int httpCode, const char * zFmt, ...){
Blob msg = empty_blob;
Blob content = empty_blob;
va_list vargs;
va_start(vargs,zFmt);
blob_vappendf(&msg, zFmt, vargs);
va_end(vargs);
blob_appendf(&content,"{\"error\":\"%j\"}", blob_str(&msg));
blob_reset(&msg);
cgi_set_content(&content);
cgi_set_status(httpCode, "Error");
cgi_set_content_type("application/json");
}
/*
** Performs bootstrapping common to the /fileedit_xyz AJAX routes.
** Returns 0 if bootstrapping fails (wrong permissions), in which
** case it has reported the error and the route should immediately
** return. Returns true on success.
*/
static int fileedit_ajax_boostrap(){
login_check_credentials();
if( !g.perm.Write ){
fileedit_ajax_error(403,"Write permissions required.");
return 0;
}
return 1;
}
/*
** Returns true if the current user is allowed to edit the given
** filename, as determined by fileedit_is_editable(), else false,
** in which case it queues up an error response and the caller
** must return immediately.
*/
static int fileedit_ajax_check_filename(const char * zFilename){
if(0==fileedit_is_editable(zFilename)){
fileedit_ajax_error(403, "File is disallowed by the "
"fileedit-glob setting.");
return 0;
}
return 1;
}
/*
** Passed the values of the "r" and "file" request properties,
** this function verifies that they are valid and populates:
**
** - *zRevUuid = the fully-expanded value of zRev (owned by the
** caller). zRevUuid may be NULL.
**
** - *vid = the RID of zRevUuid. May not be NULL.
**
** - *frid = the RID of zFilename's blob content. May not be NULL.
**
** Returns 0 if the given file is not in the given checkin or if
** fileedit_ajax_check_filename() fails, else returns true. If it
** returns false, it queues up an error response and the caller must
** return immediately.
*/
static int fileedit_ajax_setup_filerev(const char * zRev,
char ** zRevUuid,
int * vid,
const char * zFilename,
int * frid){
char * zCi = 0; /* fully-resolved checkin UUID */
char * zFileUuid; /* file UUID */
if(!fileedit_ajax_check_filename(zFilename)){
return 0;
}
*vid = symbolic_name_to_rid(zRev, "ci");
if(0==*vid){
fileedit_ajax_error(404,"Cannot resolve name as a checkin: %s",
zRev);
return 0;
}
zFileUuid = fileedit_file_uuid(zFilename, *vid, 0);
if(zFileUuid==0){
fileedit_ajax_error(404,"Checkin does not contain file.");
return 0;
}
zCi = rid_to_uuid(*vid);
*frid = fast_uuid_to_rid(zFileUuid);
fossil_free(zFileUuid);
if(zRevUuid!=0){
*zRevUuid = zCi;
}else{
fossil_free(zCi);
}
return 1;
}
/*
** WEBPAGE: fileedit_content
**
** Query parameters:
**
** file=FILENAME
** r=CHECKIN_NAME
**
** User must have Write access to use this page.
**
** Responds with the raw content of the given page. On error it
** produces a JSON response as documented for fileedit_ajax_error().
*/
void fileedit_ajax_content(){
const char * zFilename = PD("file",P("name"));
const char * zRev = P("r");
int vid, frid;
Blob content = empty_blob;
const char * zMime;
if(!fileedit_ajax_boostrap()
|| !fileedit_ajax_setup_filerev(zRev, 0, &vid,
zFilename, &frid)){
return;
}
zMime = mimetype_from_name(zFilename);
content_get(frid, &content);
cgi_set_content_type(zMime ? zMime : "application/octet-stream");
cgi_set_content(&content);
}
/*
** WEBPAGE: fileedit_preview
**
** Query parameters:
**
** file=FILENAME
** render_mode=integer (FE_RENDER_xxx) (default=FE_RENDER_GUESS)
** content=text
**
** User must have Write access to use this page.
**
** Responds with the HTML content of the preview. On error it produces
** a JSON response as documented for fileedit_ajax_error().
*/
void fileedit_ajax_preview(){
const char * zFilename = PD("file",P("name"));
const char * zContent = P("content");
int renderMode = atoi(PD("render_mode","0"));
int ln = atoi(PD("ln","0"));
int iframeHeight = atoi(PD("iframe_height","40"));
Blob content = empty_blob;
if(!fileedit_ajax_boostrap()
|| !fileedit_ajax_check_filename(zFilename)){
return;
}
cgi_set_content_type("text/html");
blob_init(&content, zContent, -1);
fileedit_render_preview(&content, zFilename,
ln ? FE_PREVIEW_LINE_NUMBERS : 0,
renderMode, iframeHeight);
}
/*
** WEBPAGE: fileedit_diff
**
** file=FILENAME
** sbs=integer (1=side-by-side or 0=unified)
** content=text
** r=checkin version
**
** User must have Write access to use this page.
**
** Responds with the HTML content of the diff. On error it produces a
** JSON response as documented for fileedit_ajax_error().
*/
void fileedit_ajax_diff(){
/*
** Reminder: we only need the filename to perform valdiation
** against fileedit_is_editable(), else this route could be
** abused to get diffs against content disallowed by the
** whitelist.
*/
const char * zFilename = PD("file",P("name"));
const char * zRev = P("r");
const char * zContent = P("content");
char * zRevUuid = 0;
int isSbs = atoi(PD("sbs","0"));
int vid, frid;
Blob content = empty_blob;
if(!fileedit_ajax_boostrap()
|| !fileedit_ajax_setup_filerev(zRev, &zRevUuid, &vid,
zFilename, &frid)){
return;
}
if(!zContent){
zContent = "";
}
cgi_set_content_type("text/html");
blob_init(&content, zContent, -1);
fileedit_render_diff(&content, frid, zRevUuid, isSbs);
fossil_free(zRevUuid);
blob_reset(&content);
}
/*
** 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.
**
|
| ︙ | ︙ | |||
1169 1170 1171 1172 1173 1174 1175 |
Blob endScript = empty_blob; /* Script code to run at the
end. This content will be
combined into a single JS
function call, thus each
entry must end with a
semicolon. */
Stmt stmt = empty_Stmt;
| < < < < < < < < < < < < < < < < < < < < < | 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 |
Blob endScript = empty_blob; /* Script code to run at the
end. This content will be
combined into a single JS
function call, thus each
entry must end with a
semicolon. */
Stmt stmt = empty_Stmt;
#define fail(EXPR) blob_appendf EXPR; goto end_footer
login_check_credentials();
if( !g.perm.Write ){
login_needed(g.anon.Write);
return;
}
db_begin_transaction();
CheckinMiniInfo_init(&cimi);
|
| ︙ | ︙ | |||
1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 |
/* On error, the error message is in the err blob and will
** be emitted at the end. */
cimi.pMfOut = 0;
blob_reset(&manifest);
break;
}
CX("<h1>Editing:</h1>");
CX("<p class='fileedit-hint'>");
CX("File: "
| > > | > | < | > | | | < | | > > > < < < | < < | 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 |
/* On error, the error message is in the err blob and will
** be emitted at the end. */
cimi.pMfOut = 0;
blob_reset(&manifest);
break;
}
CX("<div id='fossil-status-bar'>Async. status messages will go "
"here.</div>\n");
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");
/******* 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. */);
|
| ︙ | ︙ | |||
1457 1458 1459 1460 1461 1462 1463 |
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'>"
| | < | < | < | < < < < | < > | | | | | | | | | | | < < | | | | | | | < < | < | | < < < < < < < < | < < < < | < < < < < < < < < | < < < < < < < < < | < < < < < | < > > | 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 |
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-preview'>Preview</button>");
{
/* Preview rendering mode selection... */
previewRenderMode = atoi(PD("preview_render_mode","0"));
previewRenderMode = fileedit_render_mode_for_mimetype(zFileMime);
style_select_list_int("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);
previewHtmlHeight = atoi(PD("preview_html_ems","0"));
if(!previewHtmlHeight){
previewHtmlHeight = 40;
}
/* Allow selection of HTML preview iframe height */
style_select_list_int("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);
style_labeled_checkbox("preview_ln",
"Add line numbers to plain-text previews?",
"1",
"If on, plain-text files (only) will get "
"line numbers added to the preview.",
previewLn);
}
CX("<button id='fileedit-btn-diffsbs'>Diff (SBS)</button>");
CX("<button id='fileedit-btn-diffu'>Diff (Unified)</button>");
CX("</div></fieldset>");
/******* End of form *******/
CX("</form>\n");
CX("<div id='ajax-target'></div>"
/* this is where preview/diff go */);
/* Dynamically populate the editor... */
blob_appendf(&endScript,
"fossil.page.loadFile('%j','%j');",
zFilename, cimi.zParentUuid);
end_footer:
zContent = 0;
fossil_free(zFileUuid);
if(stmt.pStmt){
db_finalize(&stmt);
}
if(blob_size(&err)){
CX("<div class='fileedit-error-report'>%s</div>",
blob_str(&err));
}else if(blob_size(&submitResult)){
CX("%b",&submitResult);
}
blob_reset(&submitResult);
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/*noting that dry-run mode will have already
** set this to rollback mode. */);
style_footer();
}
|
Added src/fossil.bootstrap.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 |
"use strict";
/* Bootstrapping bits for the window.fossil object. Must be
loaded after style.c:style_emit_script_tag() has initialized
that object.
*/
/*
** By default fossil.message() sends its arguments console.debug(). If
** fossil.message.targetElement is set, it is assumed to be a DOM
** element, its innerText gets assigned to the concatenation of all
** arguments (with a space between each), and the CSS 'error' class is
** removed from the object. Pass it a falsy value to clear the target
** element.
**
** Returns this object.
*/
window.fossil.message = function f(msg){
const args = Array.prototype.slice.call(arguments,0);
const tgt = f.targetElement;
if(tgt){
tgt.classList.remove('error');
tgt.innerText = msg || args.join(' ');
}
else{
args.unshift('Fossil status:');
console.debug.apply(console,args);
}
return this;
};
/*
** Set message.targetElement to #fossil-status-bar, if found.
*/
window.fossil.message.targetElement =
document.querySelector('#fossil-status-bar');
/*
** By default fossil.error() sends its first argument to
** console.error(). If fossil.message.targetElement (yes,
** fossil.message) is set, it adds the 'error' CSS class to
** that element and sets its content as defined for message().
**
** Returns this object.
*/
window.fossil.error = function f(msg){
const args = Array.prototype.slice.call(arguments,0);
const tgt = window.fossil.message.targetElement;
if(tgt){
tgt.classList.add('error');
tgt.innerText = msg || args.join(' ');
}
else{
args.unshift('Fossil error:');
console.error.apply(console,args);
}
return this;
};
|
Added src/fossil.fetch.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 |
"use strict";
/**
Documented in style.c:style_emit_script_fetch(). Requires that
window.fossil have already been set up.
*/
window.fossil.fetch = function f(uri,opt){
if(!f.onerror){
f.onerror = function(e){
console.error("Ajax error:",e);
fossil.error("Ajax error!");
if(e.originalTarget && e.originalTarget.responseType==='text'){
const txt = e.originalTarget.responseText;
try{
/* The convention from the /filepage_xyz routes is to
** return error responses in JSON form if possible.
*/
const j = JSON.parse(txt);
console.error("Error JSON:",j);
if(j.error){ fossil.error(j.error) };
}catch(e){/*ignore: not JSON*/}
}
};
}
if('/'===uri[0]) uri = uri.substr(1);
if(!opt){
opt = {};
}else if('function'===typeof opt){
opt={onload:opt};
}
if(!opt.onload) opt.onload = (r)=>console.debug('ajax response:',r);
if(!opt.onerror) opt.onerror = f.onerror;
let payload = opt.payload, jsonResponse = false;
if(payload){
opt.method = 'POST';
if(!(payload instanceof FormData)
&& !(payload instanceof Document)
&& !(payload instanceof Blob)
&& !(payload instanceof File)
&& !(payload instanceof ArrayBuffer)){
if('object'===typeof payload || payload instanceof Array){
payload = JSON.stringify(payload);
opt.contentType = 'application/json';
}
}
}
const url=[window.fossil.rootPath+uri], x=new XMLHttpRequest();
if(opt.urlParams){
url.push('?');
if('string'===typeof opt.urlParams){
url.push(opt.urlParams);
}else{/*assume object*/
let k, i = 0;
for( k in opt.urlParams ){
if(i++) url.push('&');
url.push(k,'=',encodeURIComponent(opt.urlParams[k]));
}
}
}
if('POST'===opt.method && 'string'===typeof opt.contentType){
x.setRequestHeader('Content-Type',opt.contentType);
}
x.open(opt.method||'GET', url.join(''), true);
if('json'===opt.responseType){
jsonResponse = true;
x.responseType = 'text';
}else{
x.responseType = opt.responseType||'text';
}
if(opt.onload){
x.onload = function(e){
if(200!==this.status){
if(opt.onerror) opt.onerror(e);
return;
}
try{
opt.onload((jsonResponse && this.response)
? JSON.parse(this.response) : this.response);
}catch(e){
if(opt.onerror) opt.onerror(e);
}
}
}
if(payload) x.send(payload);
else x.send();
return this;
};
|
Added 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 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 |
(function(){
"use strict";
/**
Code for the /filepage app. Requires that the fossil JS bootstrappin
is complete and fossil.fetch() has been installed.
*/
const E = (s)=>document.querySelector(s);
window.addEventListener("load", function() {
const P = fossil.page;
P.e = {
editor: E('#fileedit-content'),
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")
};
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
);
}, 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.editor.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.editor.value,
target = this.e.ajaxContentTarget;
const updateView = function(c){
target.innerHTML = [
"<div class='fileedit-diff'>",
"<div>Diff <code>[",
self.finfo.r,
"]</code> → 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;
};
})();
|
Changes to src/main.mk.
| ︙ | ︙ | |||
216 217 218 219 220 221 222 223 224 225 226 227 228 229 | $(SRCDIR)/../skins/xekri/footer.txt \ $(SRCDIR)/../skins/xekri/header.txt \ $(SRCDIR)/accordion.js \ $(SRCDIR)/ci_edit.js \ $(SRCDIR)/copybtn.js \ $(SRCDIR)/diff.tcl \ $(SRCDIR)/forum.js \ $(SRCDIR)/graph.js \ $(SRCDIR)/href.js \ $(SRCDIR)/login.js \ $(SRCDIR)/markdown.md \ $(SRCDIR)/menu.js \ $(SRCDIR)/sbsdiff.js \ $(SRCDIR)/scroll.js \ | > > > | 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 | $(SRCDIR)/../skins/xekri/footer.txt \ $(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 \ |
| ︙ | ︙ |
Changes to src/style.c.
| ︙ | ︙ | |||
1418 1419 1420 1421 1422 1423 1424 |
** 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());
| | | > > | | > > > > > > > > > > > > > | > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > > | 1418 1419 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 1508 1509 1510 1511 1512 1513 1514 |
** 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:
**
** fossilFetch( URI, onLoadCallback );
**
** fossilFetch( URI, optionsObject );
**
** Noting that URI must be relative to the top of the repository and
** should not start with a slash (if it does, it is stripped). It gets
** the equivalent of "%R/" prepended to it.
**
** The optionsObject may be an onload callback or an object with any
** of these properties:
**
** - onload: callback(responseData) (default = output response to
** the console).
**
** - onerror: callback(XHR onload event | exception)
** (default = output 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.
**
** Returns this object, noting that the XHR request is 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);
}
}
|
Changes to win/Makefile.mingw.
| ︙ | ︙ | |||
638 639 640 641 642 643 644 645 646 647 648 649 650 651 | $(SRCDIR)/../skins/xekri/footer.txt \ $(SRCDIR)/../skins/xekri/header.txt \ $(SRCDIR)/accordion.js \ $(SRCDIR)/ci_edit.js \ $(SRCDIR)/copybtn.js \ $(SRCDIR)/diff.tcl \ $(SRCDIR)/forum.js \ $(SRCDIR)/graph.js \ $(SRCDIR)/href.js \ $(SRCDIR)/login.js \ $(SRCDIR)/markdown.md \ $(SRCDIR)/menu.js \ $(SRCDIR)/sbsdiff.js \ $(SRCDIR)/scroll.js \ | > > > | 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 | $(SRCDIR)/../skins/xekri/footer.txt \ $(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 \ |
| ︙ | ︙ |
Changes to win/Makefile.msc.
| ︙ | ︙ | |||
545 546 547 548 549 550 551 552 553 554 555 556 557 558 |
$(SRCDIR)\..\skins\xekri\footer.txt \
$(SRCDIR)\..\skins\xekri\header.txt \
$(SRCDIR)\accordion.js \
$(SRCDIR)\ci_edit.js \
$(SRCDIR)\copybtn.js \
$(SRCDIR)\diff.tcl \
$(SRCDIR)\forum.js \
$(SRCDIR)\graph.js \
$(SRCDIR)\href.js \
$(SRCDIR)\login.js \
$(SRCDIR)\markdown.md \
$(SRCDIR)\menu.js \
$(SRCDIR)\sbsdiff.js \
$(SRCDIR)\scroll.js \
| > > > | 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 |
$(SRCDIR)\..\skins\xekri\footer.txt \
$(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 \
|
| ︙ | ︙ |