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.
Commit
8e98f3e32e0de9761ebe661d27f846aad3e1d4a0cab40ca3361b6d8c96d061d1
Parent
7d7b74dd88f3b08…
2 files changed
+50
-42
+9
-1
+50
-42
| --- src/fossil.page.forumpost.js | ||
| +++ src/fossil.page.forumpost.js | ||
| @@ -371,11 +371,11 @@ | ||
| 371 | 371 | }/*constructor*/ |
| 372 | 372 | |
| 373 | 373 | discard(){ |
| 374 | 374 | const e = this.#e.widget; |
| 375 | 375 | if( e.parentNode ){ |
| 376 | - console.debug("FPE discarding", this); | |
| 376 | + //console.debug("FPE discarding", this); | |
| 377 | 377 | this.#clearDraft(); |
| 378 | 378 | e.remove(); |
| 379 | 379 | if( this.#opt.ondiscard instanceof Function ){ |
| 380 | 380 | this.#opt.ondiscard(); |
| 381 | 381 | } |
| @@ -397,19 +397,10 @@ | ||
| 397 | 397 | |
| 398 | 398 | set editorContent(v){ |
| 399 | 399 | this.#e.editor.value = v; |
| 400 | 400 | } |
| 401 | 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 | 402 | /** |
| 412 | 403 | Reports an error by appending each argument to the error widget |
| 413 | 404 | and unhiding it. If passed no arugments, it clears and hides |
| 414 | 405 | the error widget. |
| 415 | 406 | */ |
| @@ -464,16 +455,20 @@ | ||
| 464 | 455 | |
| 465 | 456 | #initAttacherTab(){ |
| 466 | 457 | this.#att = new F.Attacher({ |
| 467 | 458 | reverse: true |
| 468 | 459 | }); |
| 469 | - if( this.#opt.fpid ){ | |
| 460 | + if( this.#opt.edit ){ | |
| 470 | 461 | const eNote = D.append( |
| 471 | 462 | D.div(), |
| 472 | 463 | "Tip: attachments can be added to posts without editing them", |
| 473 | 464 | "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 | + ), | |
| 475 | 470 | ".", |
| 476 | 471 | ); |
| 477 | 472 | this.#e.tabAttach.append(eNote); |
| 478 | 473 | } |
| 479 | 474 | this.#e.tabAttach.append(this.#att.widget); |
| @@ -648,31 +643,41 @@ | ||
| 648 | 643 | if( this.#draft ){ |
| 649 | 644 | this.#draft.mtime = Date.now(); |
| 650 | 645 | F.storage.setJSON(this.#opt.draftKey, this.#draft); |
| 651 | 646 | } |
| 652 | 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 | + } | |
| 653 | 657 | |
| 654 | 658 | /** |
| 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. | |
| 658 | 663 | */ |
| 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 | + }; | |
| 662 | 673 | 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); | |
| 668 | 676 | } |
| 669 | 677 | }else{ |
| 670 | - const o = F.getJSON(key); | |
| 671 | - if( o && (o.mtime+age < now)){ | |
| 672 | - F.storage.remove(key); | |
| 673 | - } | |
| 678 | + check(key); | |
| 674 | 679 | } |
| 675 | 680 | } |
| 676 | 681 | |
| 677 | 682 | async #fetchPost(){ |
| 678 | 683 | /* |
| @@ -894,33 +899,33 @@ | ||
| 894 | 899 | if( j.error ) throw new Error(j.error); |
| 895 | 900 | return j; |
| 896 | 901 | }); |
| 897 | 902 | }; |
| 898 | 903 | |
| 899 | - const setupEditReplyElement = (ePost, eButton)=>{ | |
| 904 | + const setupEditReplyElement = (ePost, eButton, eToDisable)=>{ | |
| 900 | 905 | const fpid = ePost.dataset.fpid; |
| 901 | 906 | ePost.dataset.originalMarginLeft = ePost.style.marginLeft; |
| 902 | 907 | ePost.style.marginLeft = 'initial'; |
| 903 | - eButton.disabled = true; | |
| 904 | 908 | eButton.dataset.originalLabel = eButton.value; |
| 909 | + D.disable(eToDisable); | |
| 905 | 910 | return fpid; |
| 906 | 911 | }; |
| 907 | 912 | |
| 908 | - const restoreEditReplyElement = (ePost, eButton)=>{ | |
| 913 | + const restoreEditReplyElement = (ePost, eButton, eToDisable)=>{ | |
| 909 | 914 | if( ePost.dataset.originalMarginLeft ){ |
| 910 | 915 | ePost.style.marginLeft = ePost.dataset.originalMarginLeft; |
| 911 | 916 | delete ePost.dataset.originalMarginLeft; |
| 912 | 917 | } |
| 913 | - eButton.disabled = false; | |
| 914 | 918 | if( eButton.dataset.originalLabel ){ |
| 915 | 919 | eButton.innerText = eButton.dataset.originalLabel; |
| 916 | 920 | delete eButton.dataset.originalLabel; |
| 917 | 921 | } |
| 922 | + D.enable(eToDisable); | |
| 918 | 923 | }; |
| 919 | 924 | |
| 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); | |
| 922 | 927 | eBtnReply.innerText = "Replying..."; |
| 923 | 928 | F.toast.error("Reply is TODO. fpid="+fpid); |
| 924 | 929 | /* |
| 925 | 930 | TODOs include: |
| 926 | 931 | |
| @@ -934,15 +939,15 @@ | ||
| 934 | 939 | a Cancel button. |
| 935 | 940 | |
| 936 | 941 | - When cancelled or submitted, restore the reply button and |
| 937 | 942 | ePost position. |
| 938 | 943 | */ |
| 939 | - restoreEditReplyElement(ePost, eBtnReply); | |
| 944 | + restoreEditReplyElement(ePost, eBtnReply, eToDisable); | |
| 940 | 945 | }/*replyClicked()*/; |
| 941 | 946 | |
| 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); | |
| 944 | 949 | eBtnEdit.innerText = "Editing..."; |
| 945 | 950 | F.toast.error("Edit is TODO. fpid="+fpid); |
| 946 | 951 | /* |
| 947 | 952 | TODOs include: |
| 948 | 953 | |
| @@ -957,12 +962,12 @@ | ||
| 957 | 962 | ePost position. |
| 958 | 963 | */ |
| 959 | 964 | fetchPost(fpid) |
| 960 | 965 | .then(artifact=>{ |
| 961 | 966 | 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); | |
| 964 | 969 | if( fpe/*onsubmit*/ ){ |
| 965 | 970 | if( fpe.widget.parentNode ){ |
| 966 | 971 | fpe.widget.remove(); |
| 967 | 972 | } |
| 968 | 973 | } |
| @@ -983,35 +988,38 @@ | ||
| 983 | 988 | |
| 984 | 989 | document.body.querySelectorAll( |
| 985 | 990 | '.forumpost-single-controls > form' |
| 986 | 991 | ).forEach(form=>{ |
| 987 | 992 | //console.debug("Checking form",form); |
| 993 | + const eToDisable = []; | |
| 988 | 994 | const eThePost = form.parentElement.parentElement; |
| 989 | 995 | if( !eThePost?.dataset?.fpid ){ |
| 990 | 996 | console.warn("Unexpected missing fpid", eThePost); |
| 991 | 997 | return; |
| 992 | 998 | } |
| 993 | 999 | const btnReply = form.querySelector('input[type=submit][name=reply]'); |
| 994 | 1000 | if( btnReply ){ |
| 995 | 1001 | //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)); | |
| 997 | 1003 | b.type = 'button'/*keep container form from submitting*/; |
| 1004 | + eToDisable.push(b); | |
| 998 | 1005 | btnReply.parentElement.insertBefore(b, btnReply); |
| 999 | 1006 | btnReply.remove(); |
| 1000 | 1007 | } |
| 1001 | 1008 | const btnEdit = form.querySelector('input[type=submit][name=edit]'); |
| 1002 | 1009 | if( btnEdit ){ |
| 1003 | 1010 | //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)); | |
| 1005 | 1012 | b.type = 'button'; |
| 1013 | + eToDisable.push(b); | |
| 1006 | 1014 | btnEdit.parentElement.insertBefore(b, btnEdit); |
| 1007 | 1015 | btnEdit.remove(); |
| 1008 | 1016 | } |
| 1009 | 1017 | })/*for-each form*/; |
| 1010 | 1018 | }/* /forumpost and /forumthread */ |
| 1011 | 1019 | |
| 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. */ | |
| 1014 | 1022 | F.ForumPostEditor.purgeOldDrafts(/^draft-forum.*/); |
| 1015 | 1023 | } |
| 1016 | 1024 | })/*F.onPageLoad callback*/; |
| 1017 | 1025 | })(window.fossil); |
| 1018 | 1026 |
| --- 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 |
+9
-1
| --- src/fossil.storage.js | ||
| +++ src/fossil.storage.js | ||
| @@ -131,12 +131,20 @@ | ||
| 131 | 131 | /** Clears ALL keys from the storage. Returns this. */ |
| 132 | 132 | clear: function(){ |
| 133 | 133 | this.keys().forEach((k)=>$storage.removeItem(/*w/o prefix*/k)); |
| 134 | 134 | return this; |
| 135 | 135 | }, |
| 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. */ | |
| 137 | 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 | + }, | |
| 138 | 146 | /** Returns true if this storage is transient (only available |
| 139 | 147 | until the page is reloaded), indicating that fileStorage |
| 140 | 148 | and sessionStorage are unavailable. */ |
| 141 | 149 | isTransient: ()=>$storageHolder!==$storage, |
| 142 | 150 | /** Returns a symbolic name for the current storage mechanism. */ |
| 143 | 151 |
| --- 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 |