Fossil SCM

/fileedit now uses fileStorage or sessionStorage, if available, to store unsaved local edits to the 7 most recently edited checkin/file combinations. TODO: make that configurable and offer a widget to manage that stash and quickly switch between those files. It warns loudly if persistent storage is not available.

stephan 2020-05-15 18:43 fileedit-ajaxify
Commit d130f3568793a62193d061357b0a8ad18c61b5a8f94d2b7e7df9381c2dede998
--- src/default_css.txt
+++ src/default_css.txt
@@ -887,10 +887,16 @@
887887
color: darkred;
888888
background: yellow;
889889
}
890890
body.fileedit .error {
891891
padding: 0.25em;
892
+}
893
+body.fileedit .warning {
894
+ color: darkred;
895
+ background: yellow;
896
+ padding: 0.25em;
897
+ opacity: 0.6;
892898
}
893899
//////////////////////////////////
894900
// Styles for fossil.tabs.js:
895901
.tab-container {
896902
width: 100%;
897903
--- src/default_css.txt
+++ src/default_css.txt
@@ -887,10 +887,16 @@
887 color: darkred;
888 background: yellow;
889 }
890 body.fileedit .error {
891 padding: 0.25em;
 
 
 
 
 
 
892 }
893 //////////////////////////////////
894 // Styles for fossil.tabs.js:
895 .tab-container {
896 width: 100%;
897
--- src/default_css.txt
+++ src/default_css.txt
@@ -887,10 +887,16 @@
887 color: darkred;
888 background: yellow;
889 }
890 body.fileedit .error {
891 padding: 0.25em;
892 }
893 body.fileedit .warning {
894 color: darkred;
895 background: yellow;
896 padding: 0.25em;
897 opacity: 0.6;
898 }
899 //////////////////////////////////
900 // Styles for fossil.tabs.js:
901 .tab-container {
902 width: 100%;
903
+31 -8
--- src/fileedit.c
+++ src/fileedit.c
@@ -651,11 +651,13 @@
651651
ci_err((pErr,"File is unchanged. Not saving."));
652652
}
653653
}
654654
#if 1
655655
/* Do we really want to normalize comment EOLs? Web-posting will
656
- ** submit them in CRLF format. */
656
+ ** submit them in CRLF or LF format, depending on how exactly the
657
+ ** content is submitted (FORM (CRLF) or textarea-to-POST (LF, at
658
+ ** least in theory)). */
657659
blob_to_lf_only(&pCI->comment);
658660
#endif
659661
/* Create, save, deltify, and crosslink the manifest... */
660662
if(create_manifest_mini(&mf, pCI, pErr)==0){
661663
return 0;
@@ -1472,11 +1474,13 @@
14721474
p->flags |= CIMINI_ALLOW_FORK;
14731475
}
14741476
if(p_int("allow_older")!=0){
14751477
p->flags |= CIMINI_ALLOW_OLDER;
14761478
}
1477
- if(p_int("exec_bit")!=0){
1479
+ if(0==p_int("exec_bit")){
1480
+ p->filePerm = PERM_REG;
1481
+ }else{
14781482
p->filePerm = PERM_EXE;
14791483
}
14801484
if(p_int("allow_merge_conflict")!=0){
14811485
p->flags |= CIMINI_ALLOW_MERGE_MARKER;
14821486
}
@@ -1607,16 +1611,21 @@
16071611
** dry_run=int (1 or 0)
16081612
**
16091613
**
16101614
** User must have Write access to use this page.
16111615
**
1612
-** Responds with JSON:
1616
+** Responds with JSON (with some state repeated
1617
+** from the input in order to avoid certain race conditions
1618
+** client-side):
16131619
**
16141620
** {
1615
-** uuid: newUUID,
1621
+** checkin: newUUID,
1622
+** filename: theFilename,
1623
+** mimetype: string,
1624
+** isExe: bool,
1625
+** dryRun: bool,
16161626
** manifest: text of manifest,
1617
-** dryRun: bool
16181627
** }
16191628
**
16201629
** On error it produces a JSON response as documented for
16211630
** fileedit_ajax_error().
16221631
*/
@@ -1625,10 +1634,11 @@
16251634
Blob manifest = empty_blob; /* raw new manifest */
16261635
CheckinMiniInfo cimi; /* checkin state */
16271636
int rc; /* generic result code */
16281637
int newVid = 0; /* new version's RID */
16291638
char * zNewUuid = 0; /* newVid's UUID */
1639
+ char const * zMimetype;
16301640
16311641
if(!fileedit_ajax_boostrap()){
16321642
return;
16331643
}
16341644
db_begin_transaction();
@@ -1650,11 +1660,17 @@
16501660
}
16511661
assert(newVid>0);
16521662
zNewUuid = rid_to_uuid(newVid);
16531663
cgi_set_content_type("application/json");
16541664
CX("{");
1655
- CX("\"uuid\":%!j,", zNewUuid);
1665
+ CX("\"checkin\":%!j,", zNewUuid);
1666
+ CX("\"filename\":%!j,", cimi.zFilename);
1667
+ CX("\"isExe\": %s,", cimi.filePerm==PERM_EXE ? "true" : "false");
1668
+ zMimetype = mimetype_from_name(cimi.zFilename);
1669
+ if(zMimetype!=0){
1670
+ CX("\"mimetype\": %!j,", zMimetype);
1671
+ }
16561672
CX("\"dryRun\": %s,",
16571673
(CIMINI_DRY_RUN & cimi.flags) ? "true" : "false");
16581674
CX("\"manifest\": %!j", blob_str(&manifest));
16591675
CX("}");
16601676
db_end_transaction(0/*noting that dry-run mode will have already
@@ -1823,15 +1839,14 @@
18231839
100,
18241840
"100%", 100, "125%", 125,
18251841
"150%", 150, "175%", 175,
18261842
"200%", 200, NULL);
18271843
CX("</div>");
1828
- CX("<div class='flex-container flex-row'>");
1844
+ CX("<div class='flex-container flex-column'>");
18291845
CX("<textarea name='content' id='fileedit-content-editor' "
18301846
"class='fileedit' "
18311847
"rows='20' cols='80'>");
1832
- CX("Loading...");
18331848
CX("</textarea>");
18341849
CX("</div>"/*textarea wrapper*/);
18351850
CX("</div>"/*#tab-file-content*/);
18361851
}
18371852
@@ -2074,10 +2089,17 @@
20742089
"with many files.</li>");
20752090
CX("<li>The file selector allows, for usability's sake, only files "
20762091
"in leaf checkins to be selected, but files may be edited via "
20772092
"non-leaf checkins by passing them as the <code>filename</code> "
20782093
"and <code>checkin</code> URL arguments to this page.</li>");
2094
+ CX("<li>The editor \"stashes\" local edits to the last 7 "
2095
+ "checkin/file combinations in one of "
2096
+ "<code>window.fileStorage</code> or "
2097
+ "<code>window.sessionStorage</code>, if able, but which storage "
2098
+ "is unspecified and may differ across environments. When saving "
2099
+ "or force-reloading a file, stashed edits to that version are "
2100
+ "discarded.</li>");
20792101
CX("</ul>");
20802102
}
20812103
CX("</div>"/*#fileedit-tab-help*/);
20822104
20832105
{
@@ -2110,10 +2132,11 @@
21102132
style_emit_script_fossil_bootstrap(0);
21112133
append_diff_javascript(1);
21122134
style_emit_script_fetch(0);
21132135
style_emit_script_tabs(0)/*also emits fossil.dom*/;
21142136
style_emit_script_confirmer(0);
2137
+ style_emit_script_builtin(0, "fossil.storage.js");
21152138
style_emit_script_builtin(0, "fossil.page.fileedit.js");
21162139
if(blob_size(&endScript)>0){
21172140
style_emit_script_tag(0,0);
21182141
CX("(function(){\n");
21192142
CX("try{\n%b\n}"
21202143
--- src/fileedit.c
+++ src/fileedit.c
@@ -651,11 +651,13 @@
651 ci_err((pErr,"File is unchanged. Not saving."));
652 }
653 }
654 #if 1
655 /* Do we really want to normalize comment EOLs? Web-posting will
656 ** submit them in CRLF format. */
 
 
657 blob_to_lf_only(&pCI->comment);
658 #endif
659 /* Create, save, deltify, and crosslink the manifest... */
660 if(create_manifest_mini(&mf, pCI, pErr)==0){
661 return 0;
@@ -1472,11 +1474,13 @@
1472 p->flags |= CIMINI_ALLOW_FORK;
1473 }
1474 if(p_int("allow_older")!=0){
1475 p->flags |= CIMINI_ALLOW_OLDER;
1476 }
1477 if(p_int("exec_bit")!=0){
 
 
1478 p->filePerm = PERM_EXE;
1479 }
1480 if(p_int("allow_merge_conflict")!=0){
1481 p->flags |= CIMINI_ALLOW_MERGE_MARKER;
1482 }
@@ -1607,16 +1611,21 @@
1607 ** dry_run=int (1 or 0)
1608 **
1609 **
1610 ** User must have Write access to use this page.
1611 **
1612 ** Responds with JSON:
 
 
1613 **
1614 ** {
1615 ** uuid: newUUID,
 
 
 
 
1616 ** manifest: text of manifest,
1617 ** dryRun: bool
1618 ** }
1619 **
1620 ** On error it produces a JSON response as documented for
1621 ** fileedit_ajax_error().
1622 */
@@ -1625,10 +1634,11 @@
1625 Blob manifest = empty_blob; /* raw new manifest */
1626 CheckinMiniInfo cimi; /* checkin state */
1627 int rc; /* generic result code */
1628 int newVid = 0; /* new version's RID */
1629 char * zNewUuid = 0; /* newVid's UUID */
 
1630
1631 if(!fileedit_ajax_boostrap()){
1632 return;
1633 }
1634 db_begin_transaction();
@@ -1650,11 +1660,17 @@
1650 }
1651 assert(newVid>0);
1652 zNewUuid = rid_to_uuid(newVid);
1653 cgi_set_content_type("application/json");
1654 CX("{");
1655 CX("\"uuid\":%!j,", zNewUuid);
 
 
 
 
 
 
1656 CX("\"dryRun\": %s,",
1657 (CIMINI_DRY_RUN & cimi.flags) ? "true" : "false");
1658 CX("\"manifest\": %!j", blob_str(&manifest));
1659 CX("}");
1660 db_end_transaction(0/*noting that dry-run mode will have already
@@ -1823,15 +1839,14 @@
1823 100,
1824 "100%", 100, "125%", 125,
1825 "150%", 150, "175%", 175,
1826 "200%", 200, NULL);
1827 CX("</div>");
1828 CX("<div class='flex-container flex-row'>");
1829 CX("<textarea name='content' id='fileedit-content-editor' "
1830 "class='fileedit' "
1831 "rows='20' cols='80'>");
1832 CX("Loading...");
1833 CX("</textarea>");
1834 CX("</div>"/*textarea wrapper*/);
1835 CX("</div>"/*#tab-file-content*/);
1836 }
1837
@@ -2074,10 +2089,17 @@
2074 "with many files.</li>");
2075 CX("<li>The file selector allows, for usability's sake, only files "
2076 "in leaf checkins to be selected, but files may be edited via "
2077 "non-leaf checkins by passing them as the <code>filename</code> "
2078 "and <code>checkin</code> URL arguments to this page.</li>");
 
 
 
 
 
 
 
2079 CX("</ul>");
2080 }
2081 CX("</div>"/*#fileedit-tab-help*/);
2082
2083 {
@@ -2110,10 +2132,11 @@
2110 style_emit_script_fossil_bootstrap(0);
2111 append_diff_javascript(1);
2112 style_emit_script_fetch(0);
2113 style_emit_script_tabs(0)/*also emits fossil.dom*/;
2114 style_emit_script_confirmer(0);
 
2115 style_emit_script_builtin(0, "fossil.page.fileedit.js");
2116 if(blob_size(&endScript)>0){
2117 style_emit_script_tag(0,0);
2118 CX("(function(){\n");
2119 CX("try{\n%b\n}"
2120
--- src/fileedit.c
+++ src/fileedit.c
@@ -651,11 +651,13 @@
651 ci_err((pErr,"File is unchanged. Not saving."));
652 }
653 }
654 #if 1
655 /* Do we really want to normalize comment EOLs? Web-posting will
656 ** submit them in CRLF or LF format, depending on how exactly the
657 ** content is submitted (FORM (CRLF) or textarea-to-POST (LF, at
658 ** least in theory)). */
659 blob_to_lf_only(&pCI->comment);
660 #endif
661 /* Create, save, deltify, and crosslink the manifest... */
662 if(create_manifest_mini(&mf, pCI, pErr)==0){
663 return 0;
@@ -1472,11 +1474,13 @@
1474 p->flags |= CIMINI_ALLOW_FORK;
1475 }
1476 if(p_int("allow_older")!=0){
1477 p->flags |= CIMINI_ALLOW_OLDER;
1478 }
1479 if(0==p_int("exec_bit")){
1480 p->filePerm = PERM_REG;
1481 }else{
1482 p->filePerm = PERM_EXE;
1483 }
1484 if(p_int("allow_merge_conflict")!=0){
1485 p->flags |= CIMINI_ALLOW_MERGE_MARKER;
1486 }
@@ -1607,16 +1611,21 @@
1611 ** dry_run=int (1 or 0)
1612 **
1613 **
1614 ** User must have Write access to use this page.
1615 **
1616 ** Responds with JSON (with some state repeated
1617 ** from the input in order to avoid certain race conditions
1618 ** client-side):
1619 **
1620 ** {
1621 ** checkin: newUUID,
1622 ** filename: theFilename,
1623 ** mimetype: string,
1624 ** isExe: bool,
1625 ** dryRun: bool,
1626 ** manifest: text of manifest,
 
1627 ** }
1628 **
1629 ** On error it produces a JSON response as documented for
1630 ** fileedit_ajax_error().
1631 */
@@ -1625,10 +1634,11 @@
1634 Blob manifest = empty_blob; /* raw new manifest */
1635 CheckinMiniInfo cimi; /* checkin state */
1636 int rc; /* generic result code */
1637 int newVid = 0; /* new version's RID */
1638 char * zNewUuid = 0; /* newVid's UUID */
1639 char const * zMimetype;
1640
1641 if(!fileedit_ajax_boostrap()){
1642 return;
1643 }
1644 db_begin_transaction();
@@ -1650,11 +1660,17 @@
1660 }
1661 assert(newVid>0);
1662 zNewUuid = rid_to_uuid(newVid);
1663 cgi_set_content_type("application/json");
1664 CX("{");
1665 CX("\"checkin\":%!j,", zNewUuid);
1666 CX("\"filename\":%!j,", cimi.zFilename);
1667 CX("\"isExe\": %s,", cimi.filePerm==PERM_EXE ? "true" : "false");
1668 zMimetype = mimetype_from_name(cimi.zFilename);
1669 if(zMimetype!=0){
1670 CX("\"mimetype\": %!j,", zMimetype);
1671 }
1672 CX("\"dryRun\": %s,",
1673 (CIMINI_DRY_RUN & cimi.flags) ? "true" : "false");
1674 CX("\"manifest\": %!j", blob_str(&manifest));
1675 CX("}");
1676 db_end_transaction(0/*noting that dry-run mode will have already
@@ -1823,15 +1839,14 @@
1839 100,
1840 "100%", 100, "125%", 125,
1841 "150%", 150, "175%", 175,
1842 "200%", 200, NULL);
1843 CX("</div>");
1844 CX("<div class='flex-container flex-column'>");
1845 CX("<textarea name='content' id='fileedit-content-editor' "
1846 "class='fileedit' "
1847 "rows='20' cols='80'>");
 
1848 CX("</textarea>");
1849 CX("</div>"/*textarea wrapper*/);
1850 CX("</div>"/*#tab-file-content*/);
1851 }
1852
@@ -2074,10 +2089,17 @@
2089 "with many files.</li>");
2090 CX("<li>The file selector allows, for usability's sake, only files "
2091 "in leaf checkins to be selected, but files may be edited via "
2092 "non-leaf checkins by passing them as the <code>filename</code> "
2093 "and <code>checkin</code> URL arguments to this page.</li>");
2094 CX("<li>The editor \"stashes\" local edits to the last 7 "
2095 "checkin/file combinations in one of "
2096 "<code>window.fileStorage</code> or "
2097 "<code>window.sessionStorage</code>, if able, but which storage "
2098 "is unspecified and may differ across environments. When saving "
2099 "or force-reloading a file, stashed edits to that version are "
2100 "discarded.</li>");
2101 CX("</ul>");
2102 }
2103 CX("</div>"/*#fileedit-tab-help*/);
2104
2105 {
@@ -2110,10 +2132,11 @@
2132 style_emit_script_fossil_bootstrap(0);
2133 append_diff_javascript(1);
2134 style_emit_script_fetch(0);
2135 style_emit_script_tabs(0)/*also emits fossil.dom*/;
2136 style_emit_script_confirmer(0);
2137 style_emit_script_builtin(0, "fossil.storage.js");
2138 style_emit_script_builtin(0, "fossil.page.fileedit.js");
2139 if(blob_size(&endScript)>0){
2140 style_emit_script_tag(0,0);
2141 CX("(function(){\n");
2142 CX("try{\n%b\n}"
2143
--- src/fossil.page.fileedit.js
+++ src/fossil.page.fileedit.js
@@ -14,11 +14,11 @@
1414
checkin: UUID string,
1515
isExe: bool,
1616
mimetype: mimetype stringas determined by the fossil server.
1717
}
1818
19
- The fossil.page.value() method gets or sets the current file
19
+ The fossil.page.fileContent() method gets or sets the current file
2020
content for the page. Hypothetically, this can be overridden by
2121
skin-level JS in order to use a custom 3rd-party editing widget
2222
in place of the built-in textarea, but that is as yet untested.
2323
In order to do so the client would need to replace DOM element
2424
#fileedit-content-editor with their custom widget.
@@ -206,10 +206,11 @@
206206
);
207207
delete this.init;
208208
}
209209
}/*P.fileSelector*/;
210210
211
+
211212
/**
212213
Internal workaround to select the current preview mode
213214
and fire a change event if the value actually changes
214215
or if forceEvent is truthy.
215216
*/
@@ -312,17 +313,24 @@
312313
P.e.btnCommit.addEventListener(
313314
"click",(e)=>P.commit(), false
314315
);
315316
F.confirmer(P.e.btnReload, {
316317
confirmText: "Really reload, losing edits?",
317
- onconfirm: (e)=>P.loadFile(),
318
+ onconfirm: (e)=>P.unstashContent().loadFile(),
318319
ticks: 3
319320
});
320321
E('#comment-toggle').addEventListener(
321322
"click",(e)=>P.toggleCommentMode(), false
322323
);
323324
325
+ P.e.taEditor.addEventListener(
326
+ 'change', ()=>P.stashContentChange(), false
327
+ );
328
+ P.e.cbIsExe.addEventListener(
329
+ 'change', ()=>P.stashContentChange(true), false
330
+ );
331
+
324332
/**
325333
Cosmetic: jump through some hoops to enable/disable
326334
certain preview options depending on the current
327335
preview mode...
328336
*/
@@ -367,10 +375,29 @@
367375
'fileedit-file-loaded',
368376
(e)=>console.debug('fileedit-file-loaded ==>',e)
369377
);
370378
}
371379
380
+ /* Tell the user about which storage is being used... */
381
+ const storageMsgTarget = P.e.tabs.content;
382
+ let storageMsg = D.addClass(D.div(),'flex-container','flex-row',
383
+ 'fileedit-hint');
384
+ if(F.storage.isTransient()){
385
+ D.append(
386
+ D.addClass(storageMsg,'warning'),
387
+ "Warning: persistent storage is not avaible, "+
388
+ "so unsaved edits "+
389
+ "will not survive a page reload."
390
+ );
391
+ }else{
392
+ D.append(
393
+ storageMsg,
394
+ "Current storage mechanism for local edits: "+
395
+ F.storage.storageImplName()
396
+ );
397
+ }
398
+ storageMsgTarget.insertBefore(storageMsg, storageMsgTarget.lastElementChild);
372399
}, false)/*onload event handler*/;
373400
374401
/**
375402
Getter (if called with no args) or setter (if passed an arg) for
376403
the current file content. We use a function, rather than direct
@@ -379,15 +406,15 @@
379406
3rd-party editor widget.
380407
381408
The setter form returns this object, and re-implementations must
382409
do the same.
383410
*/
384
- P.value = function(){
411
+ P.fileContent = function(){
385412
if(0===arguments.length){
386413
return this.e.taEditor.value;
387414
}else{
388
- this.e.taEditor.value = arguments[0];
415
+ this.e.taEditor.value = arguments[0] || '';
389416
return this;
390417
}
391418
};
392419
393420
/**
@@ -457,19 +484,23 @@
457484
}
458485
s.value = c;
459486
this.e.taComment = s;
460487
D.addClass(h, 'hidden');
461488
D.removeClass(s, 'hidden');
462
- console.debug(s,h);
463489
};
464490
465491
/**
466492
Returns true if fossil.page.finfo is set, indicating that a file
467493
has been loaded, else it reports an error and returns false.
494
+
495
+ If passed a truthy value any error message about not having
496
+ a file loaded is suppressed.
468497
*/
469
- const affirmHasFile = function(){
470
- if(!P.finfo) F.error("No file is loaded.");
498
+ const affirmHasFile = function(quiet){
499
+ if(!P.finfo){
500
+ if(!quiet) F.error("No file is loaded.");
501
+ }
471502
return !!P.finfo;
472503
};
473504
474505
/**
475506
updateVersion() updates the filename and version in various UI
@@ -563,32 +594,52 @@
563594
if(0===arguments.length){
564595
if(!affirmHasFile()) return this;
565596
file = this.finfo.filename;
566597
rev = this.finfo.checkin;
567598
}
568
- delete this.finfo;
569599
const self = this;
570
- F.message("Loading content...");
571
- F.fetch('fileedit',{
600
+ const onload = (r,headers)=>{
601
+ delete self.finfo;
602
+ self.updateVersion({
603
+ filename: file,
604
+ checkin: rev,
605
+ isExe: ('x'===headers['x-fileedit-file-perm']),
606
+ mimetype: headers['content-type'].split(';').shift()
607
+ });
608
+ self.tabs.switchToTab(self.e.tabs.content);
609
+ self.e.cbIsExe.checked = self.finfo.isExe;
610
+ self.fileContent(r);
611
+ self.dispatchEvent('fileedit-file-loaded', self.finfo);
612
+ };
613
+ const semiFinfo = {filename: file, checkin: rev};
614
+ const stashFinfo = this.getStashedFinfo(semiFinfo);
615
+ if(stashFinfo){ // fake a response from the stash...
616
+ this.finfo = stashFinfo;
617
+ this.e.cbIsExe.checked = !!stashFinfo.isExe;
618
+ onload(this.contentFromStash()||'',{
619
+ 'x-fileedit-file-perm': stashFinfo.isExe ? 'x' : undefined,
620
+ 'content-type': stashFinfo.mimetype
621
+ });
622
+ F.message("Fetched from the local-edit stash:",
623
+ F.hashDigits(stashFinfo.checkin),
624
+ stashFinfo.filename);
625
+ return this;
626
+ }
627
+ F.message(
628
+ "Loading content..."
629
+ ).fetch('fileedit',{
572630
urlParams: {
573631
ajax: 'content',
574632
filename:file,
575633
checkin:rev
576634
},
577635
responseHeaders: ['x-fileedit-file-perm', 'content-type'],
578636
onload:(r,headers)=>{
579
- F.message('Loaded content.');
580
- self.updateVersion({
581
- filename: file,
582
- checkin: rev,
583
- isExe: ('x'===headers['x-fileedit-file-perm']),
584
- mimetype: headers['content-type'].split(';').shift()
585
- });
586
- self.tabs.switchToTab(self.e.tabs.content);
587
- self.e.cbIsExe.checked = self.finfo.isExe;
588
- self.value(r);
589
- self.dispatchEvent('fileedit-file-loaded', self.finfo);
637
+ onload(r,headers);
638
+ F.message('Loaded content for',
639
+ F.hashDigits(self.finfo.checkin),
640
+ self.finfo.filename);
590641
}
591642
});
592643
return this;
593644
};
594645
@@ -606,11 +657,11 @@
606657
const updateView = function(c){
607658
D.clearElement(target);
608659
if('string'===typeof c) target.innerHTML = c;
609660
if(switchToTab) self.tabs.switchToTab(self.e.tabs.preview);
610661
};
611
- return this._postPreview(this.value(), updateView);
662
+ return this._postPreview(this.fileContent(), updateView);
612663
};
613664
614665
/**
615666
Callback for use with F.connectPagePreviewers()
616667
*/
@@ -658,11 +709,11 @@
658709
659710
Returns this object, noting that the operation is async.
660711
*/
661712
P.diff = function f(sbs){
662713
if(!affirmHasFile()) return this;
663
- const content = this.value(),
714
+ const content = this.fileContent(),
664715
self = this;
665716
if(!f.target){
666717
f.target = this.e.tabs.diff.querySelector(
667718
'#fileedit-tab-diff-wrapper'
668719
);
@@ -700,47 +751,47 @@
700751
Returns this object.
701752
*/
702753
P.commit = function f(){
703754
if(!affirmHasFile()) return this;
704755
const self = this;
705
- const content = this.value(),
756
+ const content = this.fileContent(),
706757
target = document.querySelector('#fileedit-manifest'),
707758
cbDryRun = E('[name=dry_run]'),
708759
isDryRun = cbDryRun.checked,
709760
filename = this.finfo.filename;
710
- if(!f.updateView){
711
- f.updateView = function(c){
761
+ if(!f.onload){
762
+ f.onload = function(c){
763
+ const oldFinfo = JSON.parse(JSON.stringify(self.finfo))
712764
target.innerHTML = [
713765
"<h3>Manifest",
714766
(c.dryRun?" (dry run)":""),
715
- ": ", F.hashDigits(c.uuid),"</h3>",
767
+ ": ", F.hashDigits(c.checkin),"</h3>",
716768
"<code class='fileedit-manifest'>",
717769
c.manifest,
718770
"</code></pre>"
719771
].join('');
720772
const msg = [
721773
'Committed',
722774
c.dryRun ? '(dry run)' : '',
723
- '[', F.hashDigits(c.uuid) ,'].'
775
+ '[', F.hashDigits(c.checkin) ,'].'
724776
];
725777
if(!c.dryRun){
726
- msg.push('Re-activating dry-run mode.');
778
+ if(0){
779
+ msg.push('Re-activating dry-run mode.');
780
+ cbDryRun.checked = true;
781
+ }
782
+ self.unstashContent(oldFinfo);
783
+ delete c.manifest;
784
+ self.finfo = c;
727785
self.e.taComment.value = '';
728
- cbDryRun.checked = true;
729
- self.finfo.filename = filename;
730
- self.finfo.checkin = c.uuid;
731786
self.updateVersion();
732787
self.fileSelector.loadLeaves();
733788
}
734789
F.message.apply(F, msg);
735790
self.tabs.switchToTab(self.e.tabs.commit);
736791
};
737792
}
738
- if(!content){
739
- f.updateView('');
740
- return this;
741
- }
742793
const fd = new FormData();
743794
fd.append('filename',filename);
744795
fd.append('checkin', this.finfo.checkin);
745796
fd.append('content',content);
746797
fd.append('dry_run',isDryRun ? 1 : 0);
@@ -762,11 +813,11 @@
762813
'allow_merge_conflict',
763814
'prefer_delta'
764815
].forEach(function(name){
765816
var e = E('[name='+name+']');
766817
if(e){
767
- if(e.checked) fd.append(name, 1);
818
+ fd.append(name, e.checked ? 1 : 0);
768819
}else{
769820
console.error("Missing checkbox? name =",name);
770821
}
771822
});
772823
F.message(
@@ -773,11 +824,240 @@
773824
"Checking in..."
774825
).fetch('fileedit',{
775826
urlParams: {ajax: 'commit'},
776827
payload: fd,
777828
responseType: 'json',
778
- onload: f.updateView
829
+ onload: f.onload
779830
});
780831
return this;
781832
};
782
-
833
+
834
+ /**
835
+ $stash is an internal-use-only object for managing "stashed"
836
+ local edits, to help avoid that users accidentally lose content
837
+ by switching tabs or following links or some such. The basic
838
+ theory of operation is...
839
+
840
+ All "stashed" state is stored using fossil.storage.
841
+
842
+ - When the current file content is modified by the user, the
843
+ current stathe of the current P.finfo and its the content
844
+ is stashed. For the built-in editor widget, "changes" is
845
+ notified via a 'change' event. For a client-side custom
846
+ widget, the client needs to call P.stashContentChange() when
847
+ their widget triggers the equivalent of a 'change' event.
848
+
849
+ - For certain non-content updates (as of this writing, only the
850
+ is-executable checkbox), only the P.finfo stash entry is
851
+ updated, not the content (unless the content has not yet been
852
+ stashed, in which case it is also stashed so that the stash
853
+ always has matching pairs of finfo/content).
854
+
855
+ - When saving, the stashed entry for the previous version is removed
856
+ from the stash.
857
+
858
+ - When "loading", we use any stashed state for the given
859
+ checkin/file combination. When forcing a re-load of content,
860
+ any stashed entry for that combination is removed from the
861
+ stash.
862
+
863
+ - Every time P.stashContentChange() updates the stash, it is
864
+ pruned to $stash.prune.defaultMaxCount most-recently-updated
865
+ entries.
866
+
867
+ - This API often refers to "finfo objects." Those are objects
868
+ with a minimum of {checkin,filename} properties (which must be
869
+ valid), and a combination of those two properties is used as
870
+ basis for the stash keys for any given checkin/filename
871
+ combination.
872
+
873
+ The structure of the stash is a bit convoluted for efficiency's
874
+ sake: we store a map of file info (finfo) objects separately from
875
+ those files' contents because otherwise we would be required to
876
+ JSONize/de-JSONize the file content when stashing/restoring it,
877
+ and that would be horribly inefficient (meaning "battery-consuming"
878
+ on mobile devices).
879
+ */
880
+ const $stash = {
881
+ keys: {
882
+ index: F.page.name+':index'
883
+ },
884
+ /**
885
+ index: {
886
+ "CHECKIN_HASH:FILENAME": {file info w/o content}
887
+ ...
888
+ }
889
+
890
+ In F.storage we...
891
+
892
+ - Store this.index under the key this.keys.index.
893
+
894
+ - Store each file's content under the key
895
+ (P.name+'/CHECKIN_HASH:FILENAME'). These are stored separately
896
+ from the index entries to avoid having to JSONize/de-JSONize
897
+ the content. The assumption/hope is that the browser can store
898
+ those records "directly," without any intermediary
899
+ encoding/decoding going on.
900
+ */
901
+ indexKey: function(finfo){return finfo.checkin+':'+finfo.filename},
902
+ /** Returns the key for storing content for the given key suffix,
903
+ by prepending P.name to suffix. */
904
+ contentKey: function(suffix){return P.name+'/'+suffix},
905
+ /** Returns the index object, fetching it from the stash or creating
906
+ it anew on the first call. */
907
+ getIndex: function(){
908
+ if(!this.index) this.index = F.storage.getJSON(this.keys.index,{});
909
+ return this.index;
910
+ },
911
+ /**
912
+ Returns the stashed version, if any, for the given finfo object.
913
+ */
914
+ getFinfo: function(finfo){
915
+ const ndx = this.getIndex();
916
+ return ndx[this.indexKey(finfo)];
917
+ },
918
+ /** Serializes this object's index to F.storage. Returns this. */
919
+ storeIndex: function(){
920
+ if(this.index) F.storage.setJSON(this.keys.index,this.index);
921
+ return this;
922
+ },
923
+ /** Updates the stash record for the given finfo
924
+ and (optionally) content. If passed 1 arg, only
925
+ the finfo stash is updated, else both the finfo
926
+ and its contents are (re-)stashed. Returns this.
927
+ */
928
+ updateFile: function(finfo,content){
929
+ const ndx = this.getIndex(),
930
+ key = this.indexKey(finfo);
931
+ const record = ndx[key] || (ndx[key]={
932
+ checkin: finfo.checkin,
933
+ filename: finfo.filename,
934
+ mimetype: finfo.mimetype
935
+ });
936
+ record.isExe = !!finfo.isExe;
937
+ record.stashTime = new Date().getTime();
938
+ this.storeIndex();
939
+ if(arguments.length>1){
940
+ F.storage.set(this.contentKey(key), content);
941
+ }
942
+ return this;
943
+ },
944
+ /**
945
+ Returns the stashed content, if any, for the given finfo
946
+ object.
947
+ */
948
+ stashedContent: function(finfo){
949
+ return F.storage.get(this.contentKey(this.indexKey(finfo)));
950
+ },
951
+ /** Returns true if we have stashed content for the given finfo
952
+ record. */
953
+ hasStashedContent: function(finfo){
954
+ return F.storage.contains(this.contentKey(this.indexKey(finfo)));
955
+ },
956
+ /** Unstashes the given finfo record and its content.
957
+ Returns this. */
958
+ unstash: function(finfo){
959
+ const ndx = this.getIndex(),
960
+ key = this.indexKey(finfo);
961
+ delete finfo.stashTime;
962
+ delete ndx[key];
963
+ F.storage.remove(this.contentKey(key));
964
+ return this.storeIndex();
965
+ },
966
+ /**
967
+ Clears all $stash entries from F.storage. Returns this.
968
+ */
969
+ clear: function(){
970
+ const ndx = this.getIndex(),
971
+ self = this;
972
+ Object.keys(ndx).forEach(function(k){
973
+ const e = ndx[k];
974
+ delete ndx[k];
975
+ F.storage.remove(self.contentKey(k));
976
+ });
977
+ F.storage.remove(this.keys.index);
978
+ delete this.index;
979
+ return this;
980
+ },
981
+ /**
982
+ Removes all but the maxCount most-recently-updated stash
983
+ entries, where maxCount defaults to this.prune.defaultMaxCount.
984
+ */
985
+ prune: function f(maxCount){
986
+ const ndx = this.getIndex();
987
+ const li = [];
988
+ if(!maxCount || maxCount<0) maxCount = f.defaultMaxCount;
989
+ Object.keys(ndx).forEach((k)=>li.push(ndx[k]));
990
+ li.sort((l,r)=>l.stashTime - r.stashTime);
991
+ while(li.length>maxCount){
992
+ const e = li.shift();
993
+ this.unstash(e);
994
+ console.warn("Pruned oldest stash entry:",e);
995
+ }
996
+ }
997
+ };
998
+ $stash.prune.defaultMaxCount = 7;
999
+
1000
+ /**
1001
+ Updates P.finfo for certain state and stashes P.finfo, with the
1002
+ current content fetched via P.fileContent().
1003
+
1004
+ If passed truthy AND the stash already has stashed content for
1005
+ the current file, only the stashed finfo record is updated, else
1006
+ both the finfo and content are updated.
1007
+ */
1008
+ P.stashContentChange = function(onlyFinfo){
1009
+ if(affirmHasFile(true)){
1010
+ const fi = this.finfo;
1011
+ fi.isExe = this.e.cbIsExe.checked;
1012
+ if(onlyFinfo && $stash.hasStashedContent(fi)){
1013
+ $stash.updateFile(fi);
1014
+ }else{
1015
+ $stash.updateFile(fi, P.fileContent());
1016
+ }
1017
+ F.message("Stashed change to",F.hashDigits(fi.checkin),fi.filename);
1018
+ $stash.prune();
1019
+ }
1020
+ return this;
1021
+ };
1022
+
1023
+ /**
1024
+ Removes any stashed state for the current P.finfo (if set) from
1025
+ F.storage. Returns this.
1026
+ */
1027
+ P.unstashContent = function(){
1028
+ const finfo = arguments[0] || this.finfo;
1029
+ if(finfo){
1030
+ $stash.unstash(finfo);
1031
+ //console.debug("Unstashed",finfo);
1032
+ F.message("Unstashed",F.hashDigits(finfo.checkin),finfo.filename);
1033
+ }
1034
+ return this;
1035
+ };
1036
+
1037
+ /**
1038
+ Clears all stashed file state from F.storage. Returns this.
1039
+ */
1040
+ P.clearStash = function(){
1041
+ $stash.clear();
1042
+ return this;
1043
+ };
1044
+
1045
+ /**
1046
+ If stashed content for P.finfo exists, it is returned, else
1047
+ undefined is returned.
1048
+ */
1049
+ P.contentFromStash = function(){
1050
+ return affirmHasFile(true) ? $stash.stashedContent(this.finfo) : undefined;
1051
+ };
1052
+
1053
+ /**
1054
+ If a stashed version of the given finfo object exists (same
1055
+ filename/checkin values), return it, else return undefined.
1056
+ */
1057
+ P.getStashedFinfo = function(finfo){
1058
+ return $stash.getFinfo(finfo);
1059
+ };
1060
+
1061
+ P.$stash = $stash /*only for testing/debugging */;
1062
+
7831063
})(window.fossil);
7841064
7851065
ADDED src/fossil.storage.js
--- src/fossil.page.fileedit.js
+++ src/fossil.page.fileedit.js
@@ -14,11 +14,11 @@
14 checkin: UUID string,
15 isExe: bool,
16 mimetype: mimetype stringas determined by the fossil server.
17 }
18
19 The fossil.page.value() method gets or sets the current file
20 content for the page. Hypothetically, this can be overridden by
21 skin-level JS in order to use a custom 3rd-party editing widget
22 in place of the built-in textarea, but that is as yet untested.
23 In order to do so the client would need to replace DOM element
24 #fileedit-content-editor with their custom widget.
@@ -206,10 +206,11 @@
206 );
207 delete this.init;
208 }
209 }/*P.fileSelector*/;
210
 
211 /**
212 Internal workaround to select the current preview mode
213 and fire a change event if the value actually changes
214 or if forceEvent is truthy.
215 */
@@ -312,17 +313,24 @@
312 P.e.btnCommit.addEventListener(
313 "click",(e)=>P.commit(), false
314 );
315 F.confirmer(P.e.btnReload, {
316 confirmText: "Really reload, losing edits?",
317 onconfirm: (e)=>P.loadFile(),
318 ticks: 3
319 });
320 E('#comment-toggle').addEventListener(
321 "click",(e)=>P.toggleCommentMode(), false
322 );
323
 
 
 
 
 
 
 
324 /**
325 Cosmetic: jump through some hoops to enable/disable
326 certain preview options depending on the current
327 preview mode...
328 */
@@ -367,10 +375,29 @@
367 'fileedit-file-loaded',
368 (e)=>console.debug('fileedit-file-loaded ==>',e)
369 );
370 }
371
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
372 }, false)/*onload event handler*/;
373
374 /**
375 Getter (if called with no args) or setter (if passed an arg) for
376 the current file content. We use a function, rather than direct
@@ -379,15 +406,15 @@
379 3rd-party editor widget.
380
381 The setter form returns this object, and re-implementations must
382 do the same.
383 */
384 P.value = function(){
385 if(0===arguments.length){
386 return this.e.taEditor.value;
387 }else{
388 this.e.taEditor.value = arguments[0];
389 return this;
390 }
391 };
392
393 /**
@@ -457,19 +484,23 @@
457 }
458 s.value = c;
459 this.e.taComment = s;
460 D.addClass(h, 'hidden');
461 D.removeClass(s, 'hidden');
462 console.debug(s,h);
463 };
464
465 /**
466 Returns true if fossil.page.finfo is set, indicating that a file
467 has been loaded, else it reports an error and returns false.
 
 
 
468 */
469 const affirmHasFile = function(){
470 if(!P.finfo) F.error("No file is loaded.");
 
 
471 return !!P.finfo;
472 };
473
474 /**
475 updateVersion() updates the filename and version in various UI
@@ -563,32 +594,52 @@
563 if(0===arguments.length){
564 if(!affirmHasFile()) return this;
565 file = this.finfo.filename;
566 rev = this.finfo.checkin;
567 }
568 delete this.finfo;
569 const self = this;
570 F.message("Loading content...");
571 F.fetch('fileedit',{
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
572 urlParams: {
573 ajax: 'content',
574 filename:file,
575 checkin:rev
576 },
577 responseHeaders: ['x-fileedit-file-perm', 'content-type'],
578 onload:(r,headers)=>{
579 F.message('Loaded content.');
580 self.updateVersion({
581 filename: file,
582 checkin: rev,
583 isExe: ('x'===headers['x-fileedit-file-perm']),
584 mimetype: headers['content-type'].split(';').shift()
585 });
586 self.tabs.switchToTab(self.e.tabs.content);
587 self.e.cbIsExe.checked = self.finfo.isExe;
588 self.value(r);
589 self.dispatchEvent('fileedit-file-loaded', self.finfo);
590 }
591 });
592 return this;
593 };
594
@@ -606,11 +657,11 @@
606 const updateView = function(c){
607 D.clearElement(target);
608 if('string'===typeof c) target.innerHTML = c;
609 if(switchToTab) self.tabs.switchToTab(self.e.tabs.preview);
610 };
611 return this._postPreview(this.value(), updateView);
612 };
613
614 /**
615 Callback for use with F.connectPagePreviewers()
616 */
@@ -658,11 +709,11 @@
658
659 Returns this object, noting that the operation is async.
660 */
661 P.diff = function f(sbs){
662 if(!affirmHasFile()) return this;
663 const content = this.value(),
664 self = this;
665 if(!f.target){
666 f.target = this.e.tabs.diff.querySelector(
667 '#fileedit-tab-diff-wrapper'
668 );
@@ -700,47 +751,47 @@
700 Returns this object.
701 */
702 P.commit = function f(){
703 if(!affirmHasFile()) return this;
704 const self = this;
705 const content = this.value(),
706 target = document.querySelector('#fileedit-manifest'),
707 cbDryRun = E('[name=dry_run]'),
708 isDryRun = cbDryRun.checked,
709 filename = this.finfo.filename;
710 if(!f.updateView){
711 f.updateView = function(c){
 
712 target.innerHTML = [
713 "<h3>Manifest",
714 (c.dryRun?" (dry run)":""),
715 ": ", F.hashDigits(c.uuid),"</h3>",
716 "<code class='fileedit-manifest'>",
717 c.manifest,
718 "</code></pre>"
719 ].join('');
720 const msg = [
721 'Committed',
722 c.dryRun ? '(dry run)' : '',
723 '[', F.hashDigits(c.uuid) ,'].'
724 ];
725 if(!c.dryRun){
726 msg.push('Re-activating dry-run mode.');
 
 
 
 
 
 
727 self.e.taComment.value = '';
728 cbDryRun.checked = true;
729 self.finfo.filename = filename;
730 self.finfo.checkin = c.uuid;
731 self.updateVersion();
732 self.fileSelector.loadLeaves();
733 }
734 F.message.apply(F, msg);
735 self.tabs.switchToTab(self.e.tabs.commit);
736 };
737 }
738 if(!content){
739 f.updateView('');
740 return this;
741 }
742 const fd = new FormData();
743 fd.append('filename',filename);
744 fd.append('checkin', this.finfo.checkin);
745 fd.append('content',content);
746 fd.append('dry_run',isDryRun ? 1 : 0);
@@ -762,11 +813,11 @@
762 'allow_merge_conflict',
763 'prefer_delta'
764 ].forEach(function(name){
765 var e = E('[name='+name+']');
766 if(e){
767 if(e.checked) fd.append(name, 1);
768 }else{
769 console.error("Missing checkbox? name =",name);
770 }
771 });
772 F.message(
@@ -773,11 +824,240 @@
773 "Checking in..."
774 ).fetch('fileedit',{
775 urlParams: {ajax: 'commit'},
776 payload: fd,
777 responseType: 'json',
778 onload: f.updateView
779 });
780 return this;
781 };
782
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
783 })(window.fossil);
784
785 DDED src/fossil.storage.js
--- src/fossil.page.fileedit.js
+++ src/fossil.page.fileedit.js
@@ -14,11 +14,11 @@
14 checkin: UUID string,
15 isExe: bool,
16 mimetype: mimetype stringas determined by the fossil server.
17 }
18
19 The fossil.page.fileContent() method gets or sets the current file
20 content for the page. Hypothetically, this can be overridden by
21 skin-level JS in order to use a custom 3rd-party editing widget
22 in place of the built-in textarea, but that is as yet untested.
23 In order to do so the client would need to replace DOM element
24 #fileedit-content-editor with their custom widget.
@@ -206,10 +206,11 @@
206 );
207 delete this.init;
208 }
209 }/*P.fileSelector*/;
210
211
212 /**
213 Internal workaround to select the current preview mode
214 and fire a change event if the value actually changes
215 or if forceEvent is truthy.
216 */
@@ -312,17 +313,24 @@
313 P.e.btnCommit.addEventListener(
314 "click",(e)=>P.commit(), false
315 );
316 F.confirmer(P.e.btnReload, {
317 confirmText: "Really reload, losing edits?",
318 onconfirm: (e)=>P.unstashContent().loadFile(),
319 ticks: 3
320 });
321 E('#comment-toggle').addEventListener(
322 "click",(e)=>P.toggleCommentMode(), false
323 );
324
325 P.e.taEditor.addEventListener(
326 'change', ()=>P.stashContentChange(), false
327 );
328 P.e.cbIsExe.addEventListener(
329 'change', ()=>P.stashContentChange(true), false
330 );
331
332 /**
333 Cosmetic: jump through some hoops to enable/disable
334 certain preview options depending on the current
335 preview mode...
336 */
@@ -367,10 +375,29 @@
375 'fileedit-file-loaded',
376 (e)=>console.debug('fileedit-file-loaded ==>',e)
377 );
378 }
379
380 /* Tell the user about which storage is being used... */
381 const storageMsgTarget = P.e.tabs.content;
382 let storageMsg = D.addClass(D.div(),'flex-container','flex-row',
383 'fileedit-hint');
384 if(F.storage.isTransient()){
385 D.append(
386 D.addClass(storageMsg,'warning'),
387 "Warning: persistent storage is not avaible, "+
388 "so unsaved edits "+
389 "will not survive a page reload."
390 );
391 }else{
392 D.append(
393 storageMsg,
394 "Current storage mechanism for local edits: "+
395 F.storage.storageImplName()
396 );
397 }
398 storageMsgTarget.insertBefore(storageMsg, storageMsgTarget.lastElementChild);
399 }, false)/*onload event handler*/;
400
401 /**
402 Getter (if called with no args) or setter (if passed an arg) for
403 the current file content. We use a function, rather than direct
@@ -379,15 +406,15 @@
406 3rd-party editor widget.
407
408 The setter form returns this object, and re-implementations must
409 do the same.
410 */
411 P.fileContent = function(){
412 if(0===arguments.length){
413 return this.e.taEditor.value;
414 }else{
415 this.e.taEditor.value = arguments[0] || '';
416 return this;
417 }
418 };
419
420 /**
@@ -457,19 +484,23 @@
484 }
485 s.value = c;
486 this.e.taComment = s;
487 D.addClass(h, 'hidden');
488 D.removeClass(s, 'hidden');
 
489 };
490
491 /**
492 Returns true if fossil.page.finfo is set, indicating that a file
493 has been loaded, else it reports an error and returns false.
494
495 If passed a truthy value any error message about not having
496 a file loaded is suppressed.
497 */
498 const affirmHasFile = function(quiet){
499 if(!P.finfo){
500 if(!quiet) F.error("No file is loaded.");
501 }
502 return !!P.finfo;
503 };
504
505 /**
506 updateVersion() updates the filename and version in various UI
@@ -563,32 +594,52 @@
594 if(0===arguments.length){
595 if(!affirmHasFile()) return this;
596 file = this.finfo.filename;
597 rev = this.finfo.checkin;
598 }
 
599 const self = this;
600 const onload = (r,headers)=>{
601 delete self.finfo;
602 self.updateVersion({
603 filename: file,
604 checkin: rev,
605 isExe: ('x'===headers['x-fileedit-file-perm']),
606 mimetype: headers['content-type'].split(';').shift()
607 });
608 self.tabs.switchToTab(self.e.tabs.content);
609 self.e.cbIsExe.checked = self.finfo.isExe;
610 self.fileContent(r);
611 self.dispatchEvent('fileedit-file-loaded', self.finfo);
612 };
613 const semiFinfo = {filename: file, checkin: rev};
614 const stashFinfo = this.getStashedFinfo(semiFinfo);
615 if(stashFinfo){ // fake a response from the stash...
616 this.finfo = stashFinfo;
617 this.e.cbIsExe.checked = !!stashFinfo.isExe;
618 onload(this.contentFromStash()||'',{
619 'x-fileedit-file-perm': stashFinfo.isExe ? 'x' : undefined,
620 'content-type': stashFinfo.mimetype
621 });
622 F.message("Fetched from the local-edit stash:",
623 F.hashDigits(stashFinfo.checkin),
624 stashFinfo.filename);
625 return this;
626 }
627 F.message(
628 "Loading content..."
629 ).fetch('fileedit',{
630 urlParams: {
631 ajax: 'content',
632 filename:file,
633 checkin:rev
634 },
635 responseHeaders: ['x-fileedit-file-perm', 'content-type'],
636 onload:(r,headers)=>{
637 onload(r,headers);
638 F.message('Loaded content for',
639 F.hashDigits(self.finfo.checkin),
640 self.finfo.filename);
 
 
 
 
 
 
 
641 }
642 });
643 return this;
644 };
645
@@ -606,11 +657,11 @@
657 const updateView = function(c){
658 D.clearElement(target);
659 if('string'===typeof c) target.innerHTML = c;
660 if(switchToTab) self.tabs.switchToTab(self.e.tabs.preview);
661 };
662 return this._postPreview(this.fileContent(), updateView);
663 };
664
665 /**
666 Callback for use with F.connectPagePreviewers()
667 */
@@ -658,11 +709,11 @@
709
710 Returns this object, noting that the operation is async.
711 */
712 P.diff = function f(sbs){
713 if(!affirmHasFile()) return this;
714 const content = this.fileContent(),
715 self = this;
716 if(!f.target){
717 f.target = this.e.tabs.diff.querySelector(
718 '#fileedit-tab-diff-wrapper'
719 );
@@ -700,47 +751,47 @@
751 Returns this object.
752 */
753 P.commit = function f(){
754 if(!affirmHasFile()) return this;
755 const self = this;
756 const content = this.fileContent(),
757 target = document.querySelector('#fileedit-manifest'),
758 cbDryRun = E('[name=dry_run]'),
759 isDryRun = cbDryRun.checked,
760 filename = this.finfo.filename;
761 if(!f.onload){
762 f.onload = function(c){
763 const oldFinfo = JSON.parse(JSON.stringify(self.finfo))
764 target.innerHTML = [
765 "<h3>Manifest",
766 (c.dryRun?" (dry run)":""),
767 ": ", F.hashDigits(c.checkin),"</h3>",
768 "<code class='fileedit-manifest'>",
769 c.manifest,
770 "</code></pre>"
771 ].join('');
772 const msg = [
773 'Committed',
774 c.dryRun ? '(dry run)' : '',
775 '[', F.hashDigits(c.checkin) ,'].'
776 ];
777 if(!c.dryRun){
778 if(0){
779 msg.push('Re-activating dry-run mode.');
780 cbDryRun.checked = true;
781 }
782 self.unstashContent(oldFinfo);
783 delete c.manifest;
784 self.finfo = c;
785 self.e.taComment.value = '';
 
 
 
786 self.updateVersion();
787 self.fileSelector.loadLeaves();
788 }
789 F.message.apply(F, msg);
790 self.tabs.switchToTab(self.e.tabs.commit);
791 };
792 }
 
 
 
 
793 const fd = new FormData();
794 fd.append('filename',filename);
795 fd.append('checkin', this.finfo.checkin);
796 fd.append('content',content);
797 fd.append('dry_run',isDryRun ? 1 : 0);
@@ -762,11 +813,11 @@
813 'allow_merge_conflict',
814 'prefer_delta'
815 ].forEach(function(name){
816 var e = E('[name='+name+']');
817 if(e){
818 fd.append(name, e.checked ? 1 : 0);
819 }else{
820 console.error("Missing checkbox? name =",name);
821 }
822 });
823 F.message(
@@ -773,11 +824,240 @@
824 "Checking in..."
825 ).fetch('fileedit',{
826 urlParams: {ajax: 'commit'},
827 payload: fd,
828 responseType: 'json',
829 onload: f.onload
830 });
831 return this;
832 };
833
834 /**
835 $stash is an internal-use-only object for managing "stashed"
836 local edits, to help avoid that users accidentally lose content
837 by switching tabs or following links or some such. The basic
838 theory of operation is...
839
840 All "stashed" state is stored using fossil.storage.
841
842 - When the current file content is modified by the user, the
843 current stathe of the current P.finfo and its the content
844 is stashed. For the built-in editor widget, "changes" is
845 notified via a 'change' event. For a client-side custom
846 widget, the client needs to call P.stashContentChange() when
847 their widget triggers the equivalent of a 'change' event.
848
849 - For certain non-content updates (as of this writing, only the
850 is-executable checkbox), only the P.finfo stash entry is
851 updated, not the content (unless the content has not yet been
852 stashed, in which case it is also stashed so that the stash
853 always has matching pairs of finfo/content).
854
855 - When saving, the stashed entry for the previous version is removed
856 from the stash.
857
858 - When "loading", we use any stashed state for the given
859 checkin/file combination. When forcing a re-load of content,
860 any stashed entry for that combination is removed from the
861 stash.
862
863 - Every time P.stashContentChange() updates the stash, it is
864 pruned to $stash.prune.defaultMaxCount most-recently-updated
865 entries.
866
867 - This API often refers to "finfo objects." Those are objects
868 with a minimum of {checkin,filename} properties (which must be
869 valid), and a combination of those two properties is used as
870 basis for the stash keys for any given checkin/filename
871 combination.
872
873 The structure of the stash is a bit convoluted for efficiency's
874 sake: we store a map of file info (finfo) objects separately from
875 those files' contents because otherwise we would be required to
876 JSONize/de-JSONize the file content when stashing/restoring it,
877 and that would be horribly inefficient (meaning "battery-consuming"
878 on mobile devices).
879 */
880 const $stash = {
881 keys: {
882 index: F.page.name+':index'
883 },
884 /**
885 index: {
886 "CHECKIN_HASH:FILENAME": {file info w/o content}
887 ...
888 }
889
890 In F.storage we...
891
892 - Store this.index under the key this.keys.index.
893
894 - Store each file's content under the key
895 (P.name+'/CHECKIN_HASH:FILENAME'). These are stored separately
896 from the index entries to avoid having to JSONize/de-JSONize
897 the content. The assumption/hope is that the browser can store
898 those records "directly," without any intermediary
899 encoding/decoding going on.
900 */
901 indexKey: function(finfo){return finfo.checkin+':'+finfo.filename},
902 /** Returns the key for storing content for the given key suffix,
903 by prepending P.name to suffix. */
904 contentKey: function(suffix){return P.name+'/'+suffix},
905 /** Returns the index object, fetching it from the stash or creating
906 it anew on the first call. */
907 getIndex: function(){
908 if(!this.index) this.index = F.storage.getJSON(this.keys.index,{});
909 return this.index;
910 },
911 /**
912 Returns the stashed version, if any, for the given finfo object.
913 */
914 getFinfo: function(finfo){
915 const ndx = this.getIndex();
916 return ndx[this.indexKey(finfo)];
917 },
918 /** Serializes this object's index to F.storage. Returns this. */
919 storeIndex: function(){
920 if(this.index) F.storage.setJSON(this.keys.index,this.index);
921 return this;
922 },
923 /** Updates the stash record for the given finfo
924 and (optionally) content. If passed 1 arg, only
925 the finfo stash is updated, else both the finfo
926 and its contents are (re-)stashed. Returns this.
927 */
928 updateFile: function(finfo,content){
929 const ndx = this.getIndex(),
930 key = this.indexKey(finfo);
931 const record = ndx[key] || (ndx[key]={
932 checkin: finfo.checkin,
933 filename: finfo.filename,
934 mimetype: finfo.mimetype
935 });
936 record.isExe = !!finfo.isExe;
937 record.stashTime = new Date().getTime();
938 this.storeIndex();
939 if(arguments.length>1){
940 F.storage.set(this.contentKey(key), content);
941 }
942 return this;
943 },
944 /**
945 Returns the stashed content, if any, for the given finfo
946 object.
947 */
948 stashedContent: function(finfo){
949 return F.storage.get(this.contentKey(this.indexKey(finfo)));
950 },
951 /** Returns true if we have stashed content for the given finfo
952 record. */
953 hasStashedContent: function(finfo){
954 return F.storage.contains(this.contentKey(this.indexKey(finfo)));
955 },
956 /** Unstashes the given finfo record and its content.
957 Returns this. */
958 unstash: function(finfo){
959 const ndx = this.getIndex(),
960 key = this.indexKey(finfo);
961 delete finfo.stashTime;
962 delete ndx[key];
963 F.storage.remove(this.contentKey(key));
964 return this.storeIndex();
965 },
966 /**
967 Clears all $stash entries from F.storage. Returns this.
968 */
969 clear: function(){
970 const ndx = this.getIndex(),
971 self = this;
972 Object.keys(ndx).forEach(function(k){
973 const e = ndx[k];
974 delete ndx[k];
975 F.storage.remove(self.contentKey(k));
976 });
977 F.storage.remove(this.keys.index);
978 delete this.index;
979 return this;
980 },
981 /**
982 Removes all but the maxCount most-recently-updated stash
983 entries, where maxCount defaults to this.prune.defaultMaxCount.
984 */
985 prune: function f(maxCount){
986 const ndx = this.getIndex();
987 const li = [];
988 if(!maxCount || maxCount<0) maxCount = f.defaultMaxCount;
989 Object.keys(ndx).forEach((k)=>li.push(ndx[k]));
990 li.sort((l,r)=>l.stashTime - r.stashTime);
991 while(li.length>maxCount){
992 const e = li.shift();
993 this.unstash(e);
994 console.warn("Pruned oldest stash entry:",e);
995 }
996 }
997 };
998 $stash.prune.defaultMaxCount = 7;
999
1000 /**
1001 Updates P.finfo for certain state and stashes P.finfo, with the
1002 current content fetched via P.fileContent().
1003
1004 If passed truthy AND the stash already has stashed content for
1005 the current file, only the stashed finfo record is updated, else
1006 both the finfo and content are updated.
1007 */
1008 P.stashContentChange = function(onlyFinfo){
1009 if(affirmHasFile(true)){
1010 const fi = this.finfo;
1011 fi.isExe = this.e.cbIsExe.checked;
1012 if(onlyFinfo && $stash.hasStashedContent(fi)){
1013 $stash.updateFile(fi);
1014 }else{
1015 $stash.updateFile(fi, P.fileContent());
1016 }
1017 F.message("Stashed change to",F.hashDigits(fi.checkin),fi.filename);
1018 $stash.prune();
1019 }
1020 return this;
1021 };
1022
1023 /**
1024 Removes any stashed state for the current P.finfo (if set) from
1025 F.storage. Returns this.
1026 */
1027 P.unstashContent = function(){
1028 const finfo = arguments[0] || this.finfo;
1029 if(finfo){
1030 $stash.unstash(finfo);
1031 //console.debug("Unstashed",finfo);
1032 F.message("Unstashed",F.hashDigits(finfo.checkin),finfo.filename);
1033 }
1034 return this;
1035 };
1036
1037 /**
1038 Clears all stashed file state from F.storage. Returns this.
1039 */
1040 P.clearStash = function(){
1041 $stash.clear();
1042 return this;
1043 };
1044
1045 /**
1046 If stashed content for P.finfo exists, it is returned, else
1047 undefined is returned.
1048 */
1049 P.contentFromStash = function(){
1050 return affirmHasFile(true) ? $stash.stashedContent(this.finfo) : undefined;
1051 };
1052
1053 /**
1054 If a stashed version of the given finfo object exists (same
1055 filename/checkin values), return it, else return undefined.
1056 */
1057 P.getStashedFinfo = function(finfo){
1058 return $stash.getFinfo(finfo);
1059 };
1060
1061 P.$stash = $stash /*only for testing/debugging */;
1062
1063 })(window.fossil);
1064
1065 DDED src/fossil.storage.js
--- a/src/fossil.storage.js
+++ b/src/fossil.storage.js
@@ -0,0 +1,46 @@
1
+){
2
+ /**
3
+ fossil.storage is a basic wrapper around localStorage
4
+ or
5
+ sessions).
6
+ (orfossil$storage.clear(/* We must not use localStorage on0e794dbb91
7
+
8
+ ||*/') ? $storage.k) ? $storage.getItem(||*/
9
+ remove: */
10
+ clear: ()=>$storage.clear(functionreturn (
11
+ this.isTransient(
12
+ );
13
+ }function(k){
14
+ return
15
+ ).hasOwnProperty(k);
16
+ }){
17
+ /**
18
+ fossil.storage is a basic wrapper around localStorage
19
+ or
20
+ sessions).
21
+ (orfossil$storage.clear(/* We must not use localStorage on0e794dbb91
22
+
23
+ ||*/') ? $storage.k) ? $storage.getItem(||*/
24
+ remove: */
25
+ clear: ()=>$storage.clear(ge is a basic wrappe! fossil){sessionStorage(k,dflt)=>$storagebasic wrapper arou){
26
+ /**
27
+ fossil.storage torage is a basic wrap){
28
+ /**
29
+ fossil.storage is a basic wrapper around localStorage
30
+ or
31
+ sessions).
32
+ (orfossil$storage.clear(/* We must not use localStorage on0e794dbb91
33
+
34
+ ||*/') ? $storage.k) ? $storage.getItem(||*/
35
+ remove: */
36
+ clear: ()=>$storage.clear(functionreturn (
37
+ this.isTransient(
38
+ );
39
+ }function(k){
40
+ return
41
+ ).hasOwnProperty(k);
42
+ }){
43
+ /**
44
+ f(k)=>$storagebasic wrapper arou){
45
+ /**
46
+ fossil.storage is a basic wrapper
--- a/src/fossil.storage.js
+++ b/src/fossil.storage.js
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/src/fossil.storage.js
+++ b/src/fossil.storage.js
@@ -0,0 +1,46 @@
1 ){
2 /**
3 fossil.storage is a basic wrapper around localStorage
4 or
5 sessions).
6 (orfossil$storage.clear(/* We must not use localStorage on0e794dbb91
7
8 ||*/') ? $storage.k) ? $storage.getItem(||*/
9 remove: */
10 clear: ()=>$storage.clear(functionreturn (
11 this.isTransient(
12 );
13 }function(k){
14 return
15 ).hasOwnProperty(k);
16 }){
17 /**
18 fossil.storage is a basic wrapper around localStorage
19 or
20 sessions).
21 (orfossil$storage.clear(/* We must not use localStorage on0e794dbb91
22
23 ||*/') ? $storage.k) ? $storage.getItem(||*/
24 remove: */
25 clear: ()=>$storage.clear(ge is a basic wrappe! fossil){sessionStorage(k,dflt)=>$storagebasic wrapper arou){
26 /**
27 fossil.storage torage is a basic wrap){
28 /**
29 fossil.storage is a basic wrapper around localStorage
30 or
31 sessions).
32 (orfossil$storage.clear(/* We must not use localStorage on0e794dbb91
33
34 ||*/') ? $storage.k) ? $storage.getItem(||*/
35 remove: */
36 clear: ()=>$storage.clear(functionreturn (
37 this.isTransient(
38 );
39 }function(k){
40 return
41 ).hasOwnProperty(k);
42 }){
43 /**
44 f(k)=>$storagebasic wrapper arou){
45 /**
46 fossil.storage is a basic wrapper
--- src/main.mk
+++ src/main.mk
@@ -224,10 +224,11 @@
224224
$(SRCDIR)/fossil.bootstrap.js \
225225
$(SRCDIR)/fossil.confirmer.js \
226226
$(SRCDIR)/fossil.dom.js \
227227
$(SRCDIR)/fossil.fetch.js \
228228
$(SRCDIR)/fossil.page.fileedit.js \
229
+ $(SRCDIR)/fossil.storage.js \
229230
$(SRCDIR)/fossil.tabs.js \
230231
$(SRCDIR)/graph.js \
231232
$(SRCDIR)/href.js \
232233
$(SRCDIR)/login.js \
233234
$(SRCDIR)/markdown.md \
234235
--- src/main.mk
+++ src/main.mk
@@ -224,10 +224,11 @@
224 $(SRCDIR)/fossil.bootstrap.js \
225 $(SRCDIR)/fossil.confirmer.js \
226 $(SRCDIR)/fossil.dom.js \
227 $(SRCDIR)/fossil.fetch.js \
228 $(SRCDIR)/fossil.page.fileedit.js \
 
229 $(SRCDIR)/fossil.tabs.js \
230 $(SRCDIR)/graph.js \
231 $(SRCDIR)/href.js \
232 $(SRCDIR)/login.js \
233 $(SRCDIR)/markdown.md \
234
--- src/main.mk
+++ src/main.mk
@@ -224,10 +224,11 @@
224 $(SRCDIR)/fossil.bootstrap.js \
225 $(SRCDIR)/fossil.confirmer.js \
226 $(SRCDIR)/fossil.dom.js \
227 $(SRCDIR)/fossil.fetch.js \
228 $(SRCDIR)/fossil.page.fileedit.js \
229 $(SRCDIR)/fossil.storage.js \
230 $(SRCDIR)/fossil.tabs.js \
231 $(SRCDIR)/graph.js \
232 $(SRCDIR)/href.js \
233 $(SRCDIR)/login.js \
234 $(SRCDIR)/markdown.md \
235
--- win/Makefile.mingw
+++ win/Makefile.mingw
@@ -646,10 +646,11 @@
646646
$(SRCDIR)/fossil.bootstrap.js \
647647
$(SRCDIR)/fossil.confirmer.js \
648648
$(SRCDIR)/fossil.dom.js \
649649
$(SRCDIR)/fossil.fetch.js \
650650
$(SRCDIR)/fossil.page.fileedit.js \
651
+ $(SRCDIR)/fossil.storage.js \
651652
$(SRCDIR)/fossil.tabs.js \
652653
$(SRCDIR)/graph.js \
653654
$(SRCDIR)/href.js \
654655
$(SRCDIR)/login.js \
655656
$(SRCDIR)/markdown.md \
656657
--- win/Makefile.mingw
+++ win/Makefile.mingw
@@ -646,10 +646,11 @@
646 $(SRCDIR)/fossil.bootstrap.js \
647 $(SRCDIR)/fossil.confirmer.js \
648 $(SRCDIR)/fossil.dom.js \
649 $(SRCDIR)/fossil.fetch.js \
650 $(SRCDIR)/fossil.page.fileedit.js \
 
651 $(SRCDIR)/fossil.tabs.js \
652 $(SRCDIR)/graph.js \
653 $(SRCDIR)/href.js \
654 $(SRCDIR)/login.js \
655 $(SRCDIR)/markdown.md \
656
--- win/Makefile.mingw
+++ win/Makefile.mingw
@@ -646,10 +646,11 @@
646 $(SRCDIR)/fossil.bootstrap.js \
647 $(SRCDIR)/fossil.confirmer.js \
648 $(SRCDIR)/fossil.dom.js \
649 $(SRCDIR)/fossil.fetch.js \
650 $(SRCDIR)/fossil.page.fileedit.js \
651 $(SRCDIR)/fossil.storage.js \
652 $(SRCDIR)/fossil.tabs.js \
653 $(SRCDIR)/graph.js \
654 $(SRCDIR)/href.js \
655 $(SRCDIR)/login.js \
656 $(SRCDIR)/markdown.md \
657
--- win/Makefile.msc
+++ win/Makefile.msc
@@ -553,10 +553,11 @@
553553
$(SRCDIR)\fossil.bootstrap.js \
554554
$(SRCDIR)\fossil.confirmer.js \
555555
$(SRCDIR)\fossil.dom.js \
556556
$(SRCDIR)\fossil.fetch.js \
557557
$(SRCDIR)\fossil.page.fileedit.js \
558
+ $(SRCDIR)\fossil.storage.js \
558559
$(SRCDIR)\fossil.tabs.js \
559560
$(SRCDIR)\graph.js \
560561
$(SRCDIR)\href.js \
561562
$(SRCDIR)\login.js \
562563
$(SRCDIR)\markdown.md \
563564
--- win/Makefile.msc
+++ win/Makefile.msc
@@ -553,10 +553,11 @@
553 $(SRCDIR)\fossil.bootstrap.js \
554 $(SRCDIR)\fossil.confirmer.js \
555 $(SRCDIR)\fossil.dom.js \
556 $(SRCDIR)\fossil.fetch.js \
557 $(SRCDIR)\fossil.page.fileedit.js \
 
558 $(SRCDIR)\fossil.tabs.js \
559 $(SRCDIR)\graph.js \
560 $(SRCDIR)\href.js \
561 $(SRCDIR)\login.js \
562 $(SRCDIR)\markdown.md \
563
--- win/Makefile.msc
+++ win/Makefile.msc
@@ -553,10 +553,11 @@
553 $(SRCDIR)\fossil.bootstrap.js \
554 $(SRCDIR)\fossil.confirmer.js \
555 $(SRCDIR)\fossil.dom.js \
556 $(SRCDIR)\fossil.fetch.js \
557 $(SRCDIR)\fossil.page.fileedit.js \
558 $(SRCDIR)\fossil.storage.js \
559 $(SRCDIR)\fossil.tabs.js \
560 $(SRCDIR)\graph.js \
561 $(SRCDIR)\href.js \
562 $(SRCDIR)\login.js \
563 $(SRCDIR)\markdown.md \
564
--- www/fileedit-page.md
+++ www/fileedit-page.md
@@ -75,16 +75,31 @@
7575
whether or not to implement them subject to notable contributor
7676
debate. e.g. the ability to add new files or remove/rename older
7777
files.
7878
7979
80
-### `/fileedit` **Does Not Store Draft Versions While Working**
80
+### `/fileedit` **Stores Only Limited Local Edits While Working**
81
+
82
+When changes are made to a given checkin/file combination,
83
+`/fileedit` will, if possible, store them in `window.fileStorage`
84
+or `window.sessionStorage`, if available, but...
85
+
86
+- Which storage is used is unspecified and may differ across
87
+ environments.
88
+- If neither of those is available, the storage is transient and
89
+ will not survive a page reload.
90
+- It stores only the most recent last 7 checkin/file combinations
91
+ which have been modified. Note that changing the "executable bit"
92
+ is counted as a modification, but the checkin comment is not
93
+ stored separately for each file.
94
+
95
+Exactly how long `fileStorage` will survive, and how much it can hold,
96
+is environment-dependent. `sessionStorage` will survive until the
97
+current browser tab is closed, but it survives across reloads of the
98
+same tab.
8199
82
-When using `/fileedit`, if the browser's tab is closed, a link is
83
-clicked, or the user otherwise leaves the page, any current edits
84
-*will be lost*. See the note above about this feature *not* being a
85
-replacement for a full-fledged checkout.
100
+If `/filepage` determines that no peristent storage is available
86101
87102
### The Power is Yours, but...
88103
89104
> "With great power comes great responsibility."
90105
@@ -170,16 +185,16 @@
170185
*Hypothetically*, though this is currently unproven "in the wild," it
171186
is possible to replace `/filepage`'s basic text-editing widget (a
172187
`textarea` element) with a fancy 3rd-party editor widget by doing the
173188
following:
174189
175
-First, replace the `fossil.page.value()` method with a custom
190
+First, replace the `fossil.page.fileContent()` method with a custom
176191
implementation which can get and set the being-edited text from/to the
177192
custom editor widget:
178193
179194
```
180
-fossil.page.value = function(){
195
+fossil.page.fileContent = function(){
181196
if(0===arguments.length){//call as a "getter"
182197
return the text-form content of your custom widget
183198
}
184199
else{// called as a setter
185200
set the content of your custom widget to arguments[0]
@@ -198,8 +213,8 @@
198213
199214
That method must be passed a DOM element and may only be called once:
200215
it *removes itself* the first time it is called.
201216
202217
That "should" be all there is to it. When `fossil.page` needs to get
203
-the being-edited content, it will call `fossil.page.value()` with no
218
+the being-edited content, it will call `fossil.page.fileContent()` with no
204219
arguments, and when it sets the content (immediately after (re)loading
205
-a file), it will pass that content to `fossil.page.value()`.
220
+a file), it will pass that content to `fossil.page.fileContent()`.
206221
--- www/fileedit-page.md
+++ www/fileedit-page.md
@@ -75,16 +75,31 @@
75 whether or not to implement them subject to notable contributor
76 debate. e.g. the ability to add new files or remove/rename older
77 files.
78
79
80 ### `/fileedit` **Does Not Store Draft Versions While Working**
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
82 When using `/fileedit`, if the browser's tab is closed, a link is
83 clicked, or the user otherwise leaves the page, any current edits
84 *will be lost*. See the note above about this feature *not* being a
85 replacement for a full-fledged checkout.
86
87 ### The Power is Yours, but...
88
89 > "With great power comes great responsibility."
90
@@ -170,16 +185,16 @@
170 *Hypothetically*, though this is currently unproven "in the wild," it
171 is possible to replace `/filepage`'s basic text-editing widget (a
172 `textarea` element) with a fancy 3rd-party editor widget by doing the
173 following:
174
175 First, replace the `fossil.page.value()` method with a custom
176 implementation which can get and set the being-edited text from/to the
177 custom editor widget:
178
179 ```
180 fossil.page.value = function(){
181 if(0===arguments.length){//call as a "getter"
182 return the text-form content of your custom widget
183 }
184 else{// called as a setter
185 set the content of your custom widget to arguments[0]
@@ -198,8 +213,8 @@
198
199 That method must be passed a DOM element and may only be called once:
200 it *removes itself* the first time it is called.
201
202 That "should" be all there is to it. When `fossil.page` needs to get
203 the being-edited content, it will call `fossil.page.value()` with no
204 arguments, and when it sets the content (immediately after (re)loading
205 a file), it will pass that content to `fossil.page.value()`.
206
--- www/fileedit-page.md
+++ www/fileedit-page.md
@@ -75,16 +75,31 @@
75 whether or not to implement them subject to notable contributor
76 debate. e.g. the ability to add new files or remove/rename older
77 files.
78
79
80 ### `/fileedit` **Stores Only Limited Local Edits While Working**
81
82 When changes are made to a given checkin/file combination,
83 `/fileedit` will, if possible, store them in `window.fileStorage`
84 or `window.sessionStorage`, if available, but...
85
86 - Which storage is used is unspecified and may differ across
87 environments.
88 - If neither of those is available, the storage is transient and
89 will not survive a page reload.
90 - It stores only the most recent last 7 checkin/file combinations
91 which have been modified. Note that changing the "executable bit"
92 is counted as a modification, but the checkin comment is not
93 stored separately for each file.
94
95 Exactly how long `fileStorage` will survive, and how much it can hold,
96 is environment-dependent. `sessionStorage` will survive until the
97 current browser tab is closed, but it survives across reloads of the
98 same tab.
99
100 If `/filepage` determines that no peristent storage is available
 
 
 
101
102 ### The Power is Yours, but...
103
104 > "With great power comes great responsibility."
105
@@ -170,16 +185,16 @@
185 *Hypothetically*, though this is currently unproven "in the wild," it
186 is possible to replace `/filepage`'s basic text-editing widget (a
187 `textarea` element) with a fancy 3rd-party editor widget by doing the
188 following:
189
190 First, replace the `fossil.page.fileContent()` method with a custom
191 implementation which can get and set the being-edited text from/to the
192 custom editor widget:
193
194 ```
195 fossil.page.fileContent = function(){
196 if(0===arguments.length){//call as a "getter"
197 return the text-form content of your custom widget
198 }
199 else{// called as a setter
200 set the content of your custom widget to arguments[0]
@@ -198,8 +213,8 @@
213
214 That method must be passed a DOM element and may only be called once:
215 it *removes itself* the first time it is called.
216
217 That "should" be all there is to it. When `fossil.page` needs to get
218 the being-edited content, it will call `fossil.page.fileContent()` with no
219 arguments, and when it sets the content (immediately after (re)loading
220 a file), it will pass that content to `fossil.page.fileContent()`.
221

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button