Fossil SCM

Fix purging of old messages. Disable both Reply and Edit buttons when replying or editing because doing both at once would lead to madness.

stephan 2026-06-07 20:34 UTC forum-editor-2026
Commit 8e98f3e32e0de9761ebe661d27f846aad3e1d4a0cab40ca3361b6d8c96d061d1
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -371,11 +371,11 @@
371371
}/*constructor*/
372372
373373
discard(){
374374
const e = this.#e.widget;
375375
if( e.parentNode ){
376
- console.debug("FPE discarding", this);
376
+ //console.debug("FPE discarding", this);
377377
this.#clearDraft();
378378
e.remove();
379379
if( this.#opt.ondiscard instanceof Function ){
380380
this.#opt.ondiscard();
381381
}
@@ -397,19 +397,10 @@
397397
398398
set editorContent(v){
399399
this.#e.editor.value = v;
400400
}
401401
402
- /** Clears any persistent draft state. Does not clear the UI
403
- widgets. */
404
- #clearDraft(){
405
- if( this.#draft ){
406
- F.storage.remove(this.#opt.draftKey);
407
- this.#draft = F.nu();
408
- }
409
- }
410
-
411402
/**
412403
Reports an error by appending each argument to the error widget
413404
and unhiding it. If passed no arugments, it clears and hides
414405
the error widget.
415406
*/
@@ -464,16 +455,20 @@
464455
465456
#initAttacherTab(){
466457
this.#att = new F.Attacher({
467458
reverse: true
468459
});
469
- if( this.#opt.fpid ){
460
+ if( this.#opt.edit ){
470461
const eNote = D.append(
471462
D.div(),
472463
"Tip: attachments can be added to posts without editing them",
473464
"by visiting ",
474
- D.a(F.repoUrl('attachadd?target='+this.#opt.fpid), '/attachadd'),
465
+ D.attr(
466
+ D.a(F.repoUrl('attachadd?target='+this.#opt.edit.uuid), '/attachadd'),
467
+ 'target',
468
+ '_new'
469
+ ),
475470
".",
476471
);
477472
this.#e.tabAttach.append(eNote);
478473
}
479474
this.#e.tabAttach.append(this.#att.widget);
@@ -648,31 +643,41 @@
648643
if( this.#draft ){
649644
this.#draft.mtime = Date.now();
650645
F.storage.setJSON(this.#opt.draftKey, this.#draft);
651646
}
652647
}
648
+
649
+ /** Clears any persistent draft state. Does not clear the UI
650
+ widgets. */
651
+ #clearDraft(){
652
+ if( this.#draft ){
653
+ F.storage.remove(this.#opt.draftKey);
654
+ this.#draft = F.nu();
655
+ }
656
+ }
653657
654658
/**
655
- Looks for editing draft keys matching either a fixes key or a
656
- regex, and removes each matching one which is older than some
657
- fixed "best-before" date.
659
+ Looks for editing draft keys matching either a fixed key or a
660
+ regex, and removes each matching one which is older than the
661
+ given number of days. Pass days=0 to purge all entries
662
+ immediately.
658663
*/
659
- static purgeOldDrafts(key){
660
- const age = (3600 * 24 * 10/*days*/) * 1000/*ms*/;
661
- const now = Date().now();
664
+ static purgeOldDrafts(key, days=10){
665
+ const age = (3600 * 24 * days) * 1000/*ms*/;
666
+ const now = Date.now();
667
+ const check = (k)=>{
668
+ const o = F.storage.getJSON(k);
669
+ if( o && (!days || (o.mtime+age < now)) ){
670
+ F.storage.remove(k);
671
+ }
672
+ };
662673
if( key instanceof RegExp ){
663
- for(const k of F.storage.keys().filter(v=>key.test(v))){
664
- const o = F.getJSON(k);
665
- if( o && (o.mtime+age < now)){
666
- F.storage.remove(k);
667
- }
674
+ for(const k of F.storage.shortKeys().filter(v=>key.test(v))){
675
+ check(k);
668676
}
669677
}else{
670
- const o = F.getJSON(key);
671
- if( o && (o.mtime+age < now)){
672
- F.storage.remove(key);
673
- }
678
+ check(key);
674679
}
675680
}
676681
677682
async #fetchPost(){
678683
/*
@@ -894,33 +899,33 @@
894899
if( j.error ) throw new Error(j.error);
895900
return j;
896901
});
897902
};
898903
899
- const setupEditReplyElement = (ePost, eButton)=>{
904
+ const setupEditReplyElement = (ePost, eButton, eToDisable)=>{
900905
const fpid = ePost.dataset.fpid;
901906
ePost.dataset.originalMarginLeft = ePost.style.marginLeft;
902907
ePost.style.marginLeft = 'initial';
903
- eButton.disabled = true;
904908
eButton.dataset.originalLabel = eButton.value;
909
+ D.disable(eToDisable);
905910
return fpid;
906911
};
907912
908
- const restoreEditReplyElement = (ePost, eButton)=>{
913
+ const restoreEditReplyElement = (ePost, eButton, eToDisable)=>{
909914
if( ePost.dataset.originalMarginLeft ){
910915
ePost.style.marginLeft = ePost.dataset.originalMarginLeft;
911916
delete ePost.dataset.originalMarginLeft;
912917
}
913
- eButton.disabled = false;
914918
if( eButton.dataset.originalLabel ){
915919
eButton.innerText = eButton.dataset.originalLabel;
916920
delete eButton.dataset.originalLabel;
917921
}
922
+ D.enable(eToDisable);
918923
};
919924
920
- const replyClicked = (form, ePost, eBtnReply)=>{
921
- const fpid = setupEditReplyElement(ePost, eBtnReply);
925
+ const replyClicked = (form, ePost, eBtnReply, eToDisable)=>{
926
+ const fpid = setupEditReplyElement(ePost, eBtnReply, eToDisable);
922927
eBtnReply.innerText = "Replying...";
923928
F.toast.error("Reply is TODO. fpid="+fpid);
924929
/*
925930
TODOs include:
926931
@@ -934,15 +939,15 @@
934939
a Cancel button.
935940
936941
- When cancelled or submitted, restore the reply button and
937942
ePost position.
938943
*/
939
- restoreEditReplyElement(ePost, eBtnReply);
944
+ restoreEditReplyElement(ePost, eBtnReply, eToDisable);
940945
}/*replyClicked()*/;
941946
942
- const editClicked = (form, ePost, eBtnEdit)=>{
943
- const fpid = setupEditReplyElement(ePost, eBtnEdit);
947
+ const editClicked = (form, ePost, eBtnEdit, eToDisable)=>{
948
+ const fpid = setupEditReplyElement(ePost, eBtnEdit, eToDisable);
944949
eBtnEdit.innerText = "Editing...";
945950
F.toast.error("Edit is TODO. fpid="+fpid);
946951
/*
947952
TODOs include:
948953
@@ -957,12 +962,12 @@
957962
ePost position.
958963
*/
959964
fetchPost(fpid)
960965
.then(artifact=>{
961966
const ondone = (fpe)=>{
962
- restoreEditReplyElement(ePost, eBtnEdit);
963
- console.debug("ondiscard/onsubmit", fpe);
967
+ restoreEditReplyElement(ePost, eBtnEdit, eToDisable);
968
+ //console.debug("ondiscard/onsubmit", fpe, eToDisable);
964969
if( fpe/*onsubmit*/ ){
965970
if( fpe.widget.parentNode ){
966971
fpe.widget.remove();
967972
}
968973
}
@@ -983,35 +988,38 @@
983988
984989
document.body.querySelectorAll(
985990
'.forumpost-single-controls > form'
986991
).forEach(form=>{
987992
//console.debug("Checking form",form);
993
+ const eToDisable = [];
988994
const eThePost = form.parentElement.parentElement;
989995
if( !eThePost?.dataset?.fpid ){
990996
console.warn("Unexpected missing fpid", eThePost);
991997
return;
992998
}
993999
const btnReply = form.querySelector('input[type=submit][name=reply]');
9941000
if( btnReply ){
9951001
//console.debug("hacking Reply button", btnReply);
996
- const b = D.button("Reply", ()=>replyClicked(form, eThePost, b));
1002
+ const b = D.button("Reply", ()=>replyClicked(form, eThePost, b, eToDisable));
9971003
b.type = 'button'/*keep container form from submitting*/;
1004
+ eToDisable.push(b);
9981005
btnReply.parentElement.insertBefore(b, btnReply);
9991006
btnReply.remove();
10001007
}
10011008
const btnEdit = form.querySelector('input[type=submit][name=edit]');
10021009
if( btnEdit ){
10031010
//console.debug("hacking Edit button", btnEdit);
1004
- const b = D.button("Edit", ()=>editClicked(form, eThePost, b));
1011
+ const b = D.button("Edit", ()=>editClicked(form, eThePost, b, eToDisable));
10051012
b.type = 'button';
1013
+ eToDisable.push(b);
10061014
btnEdit.parentElement.insertBefore(b, btnEdit);
10071015
btnEdit.remove();
10081016
}
10091017
})/*for-each form*/;
10101018
}/* /forumpost and /forumthread */
10111019
1012
- if( 0 ){
1013
- /* TODO every now and then, not every request... */
1020
+ if( Date.now() % 17 === 0 ){
1021
+ /* Purge old drafts only every now and then. */
10141022
F.ForumPostEditor.purgeOldDrafts(/^draft-forum.*/);
10151023
}
10161024
})/*F.onPageLoad callback*/;
10171025
})(window.fossil);
10181026
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -371,11 +371,11 @@
371 }/*constructor*/
372
373 discard(){
374 const e = this.#e.widget;
375 if( e.parentNode ){
376 console.debug("FPE discarding", this);
377 this.#clearDraft();
378 e.remove();
379 if( this.#opt.ondiscard instanceof Function ){
380 this.#opt.ondiscard();
381 }
@@ -397,19 +397,10 @@
397
398 set editorContent(v){
399 this.#e.editor.value = v;
400 }
401
402 /** Clears any persistent draft state. Does not clear the UI
403 widgets. */
404 #clearDraft(){
405 if( this.#draft ){
406 F.storage.remove(this.#opt.draftKey);
407 this.#draft = F.nu();
408 }
409 }
410
411 /**
412 Reports an error by appending each argument to the error widget
413 and unhiding it. If passed no arugments, it clears and hides
414 the error widget.
415 */
@@ -464,16 +455,20 @@
464
465 #initAttacherTab(){
466 this.#att = new F.Attacher({
467 reverse: true
468 });
469 if( this.#opt.fpid ){
470 const eNote = D.append(
471 D.div(),
472 "Tip: attachments can be added to posts without editing them",
473 "by visiting ",
474 D.a(F.repoUrl('attachadd?target='+this.#opt.fpid), '/attachadd'),
 
 
 
 
475 ".",
476 );
477 this.#e.tabAttach.append(eNote);
478 }
479 this.#e.tabAttach.append(this.#att.widget);
@@ -648,31 +643,41 @@
648 if( this.#draft ){
649 this.#draft.mtime = Date.now();
650 F.storage.setJSON(this.#opt.draftKey, this.#draft);
651 }
652 }
 
 
 
 
 
 
 
 
 
653
654 /**
655 Looks for editing draft keys matching either a fixes key or a
656 regex, and removes each matching one which is older than some
657 fixed "best-before" date.
 
658 */
659 static purgeOldDrafts(key){
660 const age = (3600 * 24 * 10/*days*/) * 1000/*ms*/;
661 const now = Date().now();
 
 
 
 
 
 
662 if( key instanceof RegExp ){
663 for(const k of F.storage.keys().filter(v=>key.test(v))){
664 const o = F.getJSON(k);
665 if( o && (o.mtime+age < now)){
666 F.storage.remove(k);
667 }
668 }
669 }else{
670 const o = F.getJSON(key);
671 if( o && (o.mtime+age < now)){
672 F.storage.remove(key);
673 }
674 }
675 }
676
677 async #fetchPost(){
678 /*
@@ -894,33 +899,33 @@
894 if( j.error ) throw new Error(j.error);
895 return j;
896 });
897 };
898
899 const setupEditReplyElement = (ePost, eButton)=>{
900 const fpid = ePost.dataset.fpid;
901 ePost.dataset.originalMarginLeft = ePost.style.marginLeft;
902 ePost.style.marginLeft = 'initial';
903 eButton.disabled = true;
904 eButton.dataset.originalLabel = eButton.value;
 
905 return fpid;
906 };
907
908 const restoreEditReplyElement = (ePost, eButton)=>{
909 if( ePost.dataset.originalMarginLeft ){
910 ePost.style.marginLeft = ePost.dataset.originalMarginLeft;
911 delete ePost.dataset.originalMarginLeft;
912 }
913 eButton.disabled = false;
914 if( eButton.dataset.originalLabel ){
915 eButton.innerText = eButton.dataset.originalLabel;
916 delete eButton.dataset.originalLabel;
917 }
 
918 };
919
920 const replyClicked = (form, ePost, eBtnReply)=>{
921 const fpid = setupEditReplyElement(ePost, eBtnReply);
922 eBtnReply.innerText = "Replying...";
923 F.toast.error("Reply is TODO. fpid="+fpid);
924 /*
925 TODOs include:
926
@@ -934,15 +939,15 @@
934 a Cancel button.
935
936 - When cancelled or submitted, restore the reply button and
937 ePost position.
938 */
939 restoreEditReplyElement(ePost, eBtnReply);
940 }/*replyClicked()*/;
941
942 const editClicked = (form, ePost, eBtnEdit)=>{
943 const fpid = setupEditReplyElement(ePost, eBtnEdit);
944 eBtnEdit.innerText = "Editing...";
945 F.toast.error("Edit is TODO. fpid="+fpid);
946 /*
947 TODOs include:
948
@@ -957,12 +962,12 @@
957 ePost position.
958 */
959 fetchPost(fpid)
960 .then(artifact=>{
961 const ondone = (fpe)=>{
962 restoreEditReplyElement(ePost, eBtnEdit);
963 console.debug("ondiscard/onsubmit", fpe);
964 if( fpe/*onsubmit*/ ){
965 if( fpe.widget.parentNode ){
966 fpe.widget.remove();
967 }
968 }
@@ -983,35 +988,38 @@
983
984 document.body.querySelectorAll(
985 '.forumpost-single-controls > form'
986 ).forEach(form=>{
987 //console.debug("Checking form",form);
 
988 const eThePost = form.parentElement.parentElement;
989 if( !eThePost?.dataset?.fpid ){
990 console.warn("Unexpected missing fpid", eThePost);
991 return;
992 }
993 const btnReply = form.querySelector('input[type=submit][name=reply]');
994 if( btnReply ){
995 //console.debug("hacking Reply button", btnReply);
996 const b = D.button("Reply", ()=>replyClicked(form, eThePost, b));
997 b.type = 'button'/*keep container form from submitting*/;
 
998 btnReply.parentElement.insertBefore(b, btnReply);
999 btnReply.remove();
1000 }
1001 const btnEdit = form.querySelector('input[type=submit][name=edit]');
1002 if( btnEdit ){
1003 //console.debug("hacking Edit button", btnEdit);
1004 const b = D.button("Edit", ()=>editClicked(form, eThePost, b));
1005 b.type = 'button';
 
1006 btnEdit.parentElement.insertBefore(b, btnEdit);
1007 btnEdit.remove();
1008 }
1009 })/*for-each form*/;
1010 }/* /forumpost and /forumthread */
1011
1012 if( 0 ){
1013 /* TODO every now and then, not every request... */
1014 F.ForumPostEditor.purgeOldDrafts(/^draft-forum.*/);
1015 }
1016 })/*F.onPageLoad callback*/;
1017 })(window.fossil);
1018
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -371,11 +371,11 @@
371 }/*constructor*/
372
373 discard(){
374 const e = this.#e.widget;
375 if( e.parentNode ){
376 //console.debug("FPE discarding", this);
377 this.#clearDraft();
378 e.remove();
379 if( this.#opt.ondiscard instanceof Function ){
380 this.#opt.ondiscard();
381 }
@@ -397,19 +397,10 @@
397
398 set editorContent(v){
399 this.#e.editor.value = v;
400 }
401
 
 
 
 
 
 
 
 
 
402 /**
403 Reports an error by appending each argument to the error widget
404 and unhiding it. If passed no arugments, it clears and hides
405 the error widget.
406 */
@@ -464,16 +455,20 @@
455
456 #initAttacherTab(){
457 this.#att = new F.Attacher({
458 reverse: true
459 });
460 if( this.#opt.edit ){
461 const eNote = D.append(
462 D.div(),
463 "Tip: attachments can be added to posts without editing them",
464 "by visiting ",
465 D.attr(
466 D.a(F.repoUrl('attachadd?target='+this.#opt.edit.uuid), '/attachadd'),
467 'target',
468 '_new'
469 ),
470 ".",
471 );
472 this.#e.tabAttach.append(eNote);
473 }
474 this.#e.tabAttach.append(this.#att.widget);
@@ -648,31 +643,41 @@
643 if( this.#draft ){
644 this.#draft.mtime = Date.now();
645 F.storage.setJSON(this.#opt.draftKey, this.#draft);
646 }
647 }
648
649 /** Clears any persistent draft state. Does not clear the UI
650 widgets. */
651 #clearDraft(){
652 if( this.#draft ){
653 F.storage.remove(this.#opt.draftKey);
654 this.#draft = F.nu();
655 }
656 }
657
658 /**
659 Looks for editing draft keys matching either a fixed key or a
660 regex, and removes each matching one which is older than the
661 given number of days. Pass days=0 to purge all entries
662 immediately.
663 */
664 static purgeOldDrafts(key, days=10){
665 const age = (3600 * 24 * days) * 1000/*ms*/;
666 const now = Date.now();
667 const check = (k)=>{
668 const o = F.storage.getJSON(k);
669 if( o && (!days || (o.mtime+age < now)) ){
670 F.storage.remove(k);
671 }
672 };
673 if( key instanceof RegExp ){
674 for(const k of F.storage.shortKeys().filter(v=>key.test(v))){
675 check(k);
 
 
 
676 }
677 }else{
678 check(key);
 
 
 
679 }
680 }
681
682 async #fetchPost(){
683 /*
@@ -894,33 +899,33 @@
899 if( j.error ) throw new Error(j.error);
900 return j;
901 });
902 };
903
904 const setupEditReplyElement = (ePost, eButton, eToDisable)=>{
905 const fpid = ePost.dataset.fpid;
906 ePost.dataset.originalMarginLeft = ePost.style.marginLeft;
907 ePost.style.marginLeft = 'initial';
 
908 eButton.dataset.originalLabel = eButton.value;
909 D.disable(eToDisable);
910 return fpid;
911 };
912
913 const restoreEditReplyElement = (ePost, eButton, eToDisable)=>{
914 if( ePost.dataset.originalMarginLeft ){
915 ePost.style.marginLeft = ePost.dataset.originalMarginLeft;
916 delete ePost.dataset.originalMarginLeft;
917 }
 
918 if( eButton.dataset.originalLabel ){
919 eButton.innerText = eButton.dataset.originalLabel;
920 delete eButton.dataset.originalLabel;
921 }
922 D.enable(eToDisable);
923 };
924
925 const replyClicked = (form, ePost, eBtnReply, eToDisable)=>{
926 const fpid = setupEditReplyElement(ePost, eBtnReply, eToDisable);
927 eBtnReply.innerText = "Replying...";
928 F.toast.error("Reply is TODO. fpid="+fpid);
929 /*
930 TODOs include:
931
@@ -934,15 +939,15 @@
939 a Cancel button.
940
941 - When cancelled or submitted, restore the reply button and
942 ePost position.
943 */
944 restoreEditReplyElement(ePost, eBtnReply, eToDisable);
945 }/*replyClicked()*/;
946
947 const editClicked = (form, ePost, eBtnEdit, eToDisable)=>{
948 const fpid = setupEditReplyElement(ePost, eBtnEdit, eToDisable);
949 eBtnEdit.innerText = "Editing...";
950 F.toast.error("Edit is TODO. fpid="+fpid);
951 /*
952 TODOs include:
953
@@ -957,12 +962,12 @@
962 ePost position.
963 */
964 fetchPost(fpid)
965 .then(artifact=>{
966 const ondone = (fpe)=>{
967 restoreEditReplyElement(ePost, eBtnEdit, eToDisable);
968 //console.debug("ondiscard/onsubmit", fpe, eToDisable);
969 if( fpe/*onsubmit*/ ){
970 if( fpe.widget.parentNode ){
971 fpe.widget.remove();
972 }
973 }
@@ -983,35 +988,38 @@
988
989 document.body.querySelectorAll(
990 '.forumpost-single-controls > form'
991 ).forEach(form=>{
992 //console.debug("Checking form",form);
993 const eToDisable = [];
994 const eThePost = form.parentElement.parentElement;
995 if( !eThePost?.dataset?.fpid ){
996 console.warn("Unexpected missing fpid", eThePost);
997 return;
998 }
999 const btnReply = form.querySelector('input[type=submit][name=reply]');
1000 if( btnReply ){
1001 //console.debug("hacking Reply button", btnReply);
1002 const b = D.button("Reply", ()=>replyClicked(form, eThePost, b, eToDisable));
1003 b.type = 'button'/*keep container form from submitting*/;
1004 eToDisable.push(b);
1005 btnReply.parentElement.insertBefore(b, btnReply);
1006 btnReply.remove();
1007 }
1008 const btnEdit = form.querySelector('input[type=submit][name=edit]');
1009 if( btnEdit ){
1010 //console.debug("hacking Edit button", btnEdit);
1011 const b = D.button("Edit", ()=>editClicked(form, eThePost, b, eToDisable));
1012 b.type = 'button';
1013 eToDisable.push(b);
1014 btnEdit.parentElement.insertBefore(b, btnEdit);
1015 btnEdit.remove();
1016 }
1017 })/*for-each form*/;
1018 }/* /forumpost and /forumthread */
1019
1020 if( Date.now() % 17 === 0 ){
1021 /* Purge old drafts only every now and then. */
1022 F.ForumPostEditor.purgeOldDrafts(/^draft-forum.*/);
1023 }
1024 })/*F.onPageLoad callback*/;
1025 })(window.fossil);
1026
--- src/fossil.storage.js
+++ src/fossil.storage.js
@@ -131,12 +131,20 @@
131131
/** Clears ALL keys from the storage. Returns this. */
132132
clear: function(){
133133
this.keys().forEach((k)=>$storage.removeItem(/*w/o prefix*/k));
134134
return this;
135135
},
136
- /** Returns an array of all keys currently in the storage. */
136
+ /** Returns an array of all keys currently in the storage. These
137
+ include the storage key prefix. */
137138
keys: ()=>Object.keys($storageHolder).filter((v)=>(v||'').startsWith(storageKeyPrefix)),
139
+ /**
140
+ Like this.keys() but returns the keys shorn of the key prefix.
141
+ */
142
+ shortKeys: function(){
143
+ const n = this.storageKeyPrefix.length;
144
+ return this.keys().map(v=>v.substring(n));
145
+ },
138146
/** Returns true if this storage is transient (only available
139147
until the page is reloaded), indicating that fileStorage
140148
and sessionStorage are unavailable. */
141149
isTransient: ()=>$storageHolder!==$storage,
142150
/** Returns a symbolic name for the current storage mechanism. */
143151
--- src/fossil.storage.js
+++ src/fossil.storage.js
@@ -131,12 +131,20 @@
131 /** Clears ALL keys from the storage. Returns this. */
132 clear: function(){
133 this.keys().forEach((k)=>$storage.removeItem(/*w/o prefix*/k));
134 return this;
135 },
136 /** Returns an array of all keys currently in the storage. */
 
137 keys: ()=>Object.keys($storageHolder).filter((v)=>(v||'').startsWith(storageKeyPrefix)),
 
 
 
 
 
 
 
138 /** Returns true if this storage is transient (only available
139 until the page is reloaded), indicating that fileStorage
140 and sessionStorage are unavailable. */
141 isTransient: ()=>$storageHolder!==$storage,
142 /** Returns a symbolic name for the current storage mechanism. */
143
--- src/fossil.storage.js
+++ src/fossil.storage.js
@@ -131,12 +131,20 @@
131 /** Clears ALL keys from the storage. Returns this. */
132 clear: function(){
133 this.keys().forEach((k)=>$storage.removeItem(/*w/o prefix*/k));
134 return this;
135 },
136 /** Returns an array of all keys currently in the storage. These
137 include the storage key prefix. */
138 keys: ()=>Object.keys($storageHolder).filter((v)=>(v||'').startsWith(storageKeyPrefix)),
139 /**
140 Like this.keys() but returns the keys shorn of the key prefix.
141 */
142 shortKeys: function(){
143 const n = this.storageKeyPrefix.length;
144 return this.keys().map(v=>v.substring(n));
145 },
146 /** Returns true if this storage is transient (only available
147 until the page is reloaded), indicating that fileStorage
148 and sessionStorage are unavailable. */
149 isTransient: ()=>$storageHolder!==$storage,
150 /** Returns a symbolic name for the current storage mechanism. */
151

Keyboard Shortcuts

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