Fossil SCM

Incremental (does-not-yet-)work on hooking into the edit/reply buttons.

stephan 2026-06-07 17:38 UTC forum-editor-2026
Commit 968832014380358624478829d36a481b57735d489de5687a0b51235fefa5ebaa
+4 -5
--- src/ajax.c
+++ src/ajax.c
@@ -313,12 +313,10 @@
313313
** AJAX_RENDER_PLAIN_TEXT mode.
314314
**
315315
** iframe_height=integer (default=40) Height, in EMs of HTML preview
316316
** iframe.
317317
**
318
-** User must have Write access to use this page.
319
-**
320318
** Responds with the HTML content of the preview. On error it produces
321319
** a JSON response as documented for ajax_route_error().
322320
**
323321
** Extra response headers:
324322
**
@@ -451,13 +449,14 @@
451449
AjaxRoute routeName = {0,0,0,0};
452450
const AjaxRoute * pRoute = 0;
453451
const AjaxRoute routes[] = {
454452
/* Keep these sorted by zName (for bsearch()) */
455453
{"preview-text", ajax_route_preview_text, 0, 1
456
- /* Note that this does not require write permissions in the repo.
457
- ** It should arguably require write permissions but doing means
458
- ** that /chat does not work without check-in permissions:
454
+ /* Preview does not require write permissions in the repo. It
455
+ ** should arguably require write permissions simply to limit abuse
456
+ ** but doing means that /chat does not work without check-in
457
+ ** permissions:
459458
**
460459
** https://fossil-scm.org/forum/forumpost/ed4a762b3a557898
461460
**
462461
** This particular route is used by /fileedit and /chat, whereas
463462
** /wikiedit uses a simpler wiki-specific route.
464463
--- src/ajax.c
+++ src/ajax.c
@@ -313,12 +313,10 @@
313 ** AJAX_RENDER_PLAIN_TEXT mode.
314 **
315 ** iframe_height=integer (default=40) Height, in EMs of HTML preview
316 ** iframe.
317 **
318 ** User must have Write access to use this page.
319 **
320 ** Responds with the HTML content of the preview. On error it produces
321 ** a JSON response as documented for ajax_route_error().
322 **
323 ** Extra response headers:
324 **
@@ -451,13 +449,14 @@
451 AjaxRoute routeName = {0,0,0,0};
452 const AjaxRoute * pRoute = 0;
453 const AjaxRoute routes[] = {
454 /* Keep these sorted by zName (for bsearch()) */
455 {"preview-text", ajax_route_preview_text, 0, 1
456 /* Note that this does not require write permissions in the repo.
457 ** It should arguably require write permissions but doing means
458 ** that /chat does not work without check-in permissions:
 
459 **
460 ** https://fossil-scm.org/forum/forumpost/ed4a762b3a557898
461 **
462 ** This particular route is used by /fileedit and /chat, whereas
463 ** /wikiedit uses a simpler wiki-specific route.
464
--- src/ajax.c
+++ src/ajax.c
@@ -313,12 +313,10 @@
313 ** AJAX_RENDER_PLAIN_TEXT mode.
314 **
315 ** iframe_height=integer (default=40) Height, in EMs of HTML preview
316 ** iframe.
317 **
 
 
318 ** Responds with the HTML content of the preview. On error it produces
319 ** a JSON response as documented for ajax_route_error().
320 **
321 ** Extra response headers:
322 **
@@ -451,13 +449,14 @@
449 AjaxRoute routeName = {0,0,0,0};
450 const AjaxRoute * pRoute = 0;
451 const AjaxRoute routes[] = {
452 /* Keep these sorted by zName (for bsearch()) */
453 {"preview-text", ajax_route_preview_text, 0, 1
454 /* Preview does not require write permissions in the repo. It
455 ** should arguably require write permissions simply to limit abuse
456 ** but doing means that /chat does not work without check-in
457 ** permissions:
458 **
459 ** https://fossil-scm.org/forum/forumpost/ed4a762b3a557898
460 **
461 ** This particular route is used by /fileedit and /chat, whereas
462 ** /wikiedit uses a simpler wiki-specific route.
463
--- src/fossil.bootstrap.js
+++ src/fossil.bootstrap.js
@@ -58,11 +58,11 @@
5858
** removed from the object. Pass it a falsy value to clear the target
5959
** element.
6060
**
6161
** Returns this object.
6262
*/
63
- F.message = function f(msg){
63
+ F.message = function f(){
6464
const args = Array.prototype.slice.call(arguments,0);
6565
const tgt = f.targetElement;
6666
if(args.length) args.unshift(
6767
localTimeString()+':'
6868
//timestring(),'UTC:'
@@ -88,26 +88,25 @@
8888
F.message.targetElement.addEventListener(
8989
'dblclick', ()=>F.message(), false
9090
);
9191
}
9292
/*
93
- ** By default fossil.error() sends its first argument to
93
+ ** By default fossil.error() sends all arguments to
9494
** console.error(). If fossil.message.targetElement (yes,
9595
** fossil.message) is set, it adds the 'error' CSS class to
9696
** that element and sets its content as defined for message().
9797
**
9898
** Returns this object.
9999
*/
100
- F.error = function f(msg){
100
+ F.error = function f(){
101101
const args = Array.prototype.slice.call(arguments,0);
102102
const tgt = F.message.targetElement;
103103
args.unshift(timestring(),'UTC:');
104104
if(tgt){
105105
tgt.classList.add('error');
106106
tgt.innerText = args.join(' ');
107
- }
108
- else{
107
+ }else{
109108
args.unshift('Fossil error:');
110109
console.error.apply(console,args);
111110
}
112111
return this;
113112
};
114113
--- src/fossil.bootstrap.js
+++ src/fossil.bootstrap.js
@@ -58,11 +58,11 @@
58 ** removed from the object. Pass it a falsy value to clear the target
59 ** element.
60 **
61 ** Returns this object.
62 */
63 F.message = function f(msg){
64 const args = Array.prototype.slice.call(arguments,0);
65 const tgt = f.targetElement;
66 if(args.length) args.unshift(
67 localTimeString()+':'
68 //timestring(),'UTC:'
@@ -88,26 +88,25 @@
88 F.message.targetElement.addEventListener(
89 'dblclick', ()=>F.message(), false
90 );
91 }
92 /*
93 ** By default fossil.error() sends its first argument to
94 ** console.error(). If fossil.message.targetElement (yes,
95 ** fossil.message) is set, it adds the 'error' CSS class to
96 ** that element and sets its content as defined for message().
97 **
98 ** Returns this object.
99 */
100 F.error = function f(msg){
101 const args = Array.prototype.slice.call(arguments,0);
102 const tgt = F.message.targetElement;
103 args.unshift(timestring(),'UTC:');
104 if(tgt){
105 tgt.classList.add('error');
106 tgt.innerText = args.join(' ');
107 }
108 else{
109 args.unshift('Fossil error:');
110 console.error.apply(console,args);
111 }
112 return this;
113 };
114
--- src/fossil.bootstrap.js
+++ src/fossil.bootstrap.js
@@ -58,11 +58,11 @@
58 ** removed from the object. Pass it a falsy value to clear the target
59 ** element.
60 **
61 ** Returns this object.
62 */
63 F.message = function f(){
64 const args = Array.prototype.slice.call(arguments,0);
65 const tgt = f.targetElement;
66 if(args.length) args.unshift(
67 localTimeString()+':'
68 //timestring(),'UTC:'
@@ -88,26 +88,25 @@
88 F.message.targetElement.addEventListener(
89 'dblclick', ()=>F.message(), false
90 );
91 }
92 /*
93 ** By default fossil.error() sends all arguments to
94 ** console.error(). If fossil.message.targetElement (yes,
95 ** fossil.message) is set, it adds the 'error' CSS class to
96 ** that element and sets its content as defined for message().
97 **
98 ** Returns this object.
99 */
100 F.error = function f(){
101 const args = Array.prototype.slice.call(arguments,0);
102 const tgt = F.message.targetElement;
103 args.unshift(timestring(),'UTC:');
104 if(tgt){
105 tgt.classList.add('error');
106 tgt.innerText = args.join(' ');
107 }else{
 
108 args.unshift('Fossil error:');
109 console.error.apply(console,args);
110 }
111 return this;
112 };
113
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -38,19 +38,33 @@
3838
3939
opt.draftKey[string=undefined]: if set then this object's state
4040
will be stored in fossil.storage when the relevant input fields
4141
lose focus. If old state is found, the form is pre-populated
4242
from it. The state is cleared on a successful submit.
43
+
44
+ opt.ondiscard[=function]: if set, a Discard button is added
45
+ which, when activated, clears the current draft and removes
46
+ this object's widget from the DOM. After that, opt.ondiscard()
47
+ is called and passed no arguments.
48
+
49
+ TODO:
50
+
51
+ opt.inReplyTo=uuid: if this is a new response to a post, this
52
+ is the full forum post uuid of the being-replied-to post.
53
+
54
+ opt.edit=artifactObject: if this is an edit of an existing
55
+ post, this is the full JSON-format artifact of the forum post
56
+ the being-edit post.
4357
*/
4458
constructor(opt){
4559
opt = this.#opt = F.nu({
4660
// todo: defaults once we determine the options
47
- // replyTo: hash
61
+ // inReplyTo: hash
4862
// fpid: hash
4963
draftKey: undefined
5064
}, opt);
51
- opt.isNewThread = !opt.replyTo && !opt.edit;
65
+ opt.isNewThread = !opt.inReplyTo && !opt.edit;
5266
if( opt.draftKey ){
5367
this.#draft = F.nu(F.storage.getJSON(opt.draftKey, {}));
5468
}
5569
const e = this.#e = F.nu({
5670
mimetype: F.nu(),
@@ -75,13 +89,13 @@
7589
if( this.#draft ){
7690
e.title.addEventListener('blur', ()=>{
7791
this.#draft.title = e.title.value;
7892
this.#storeDraft();
7993
});
80
- e.title.value = opt.title || this.#draft.title || '';
81
- }else if( opt.title ){
82
- e.title.value = opt.title;
94
+ e.title.value = opt.edit?.H || this.#draft.title || '';
95
+ }else if( opt.edit?.H ){
96
+ e.title.value = opt.edit.H;
8397
}
8498
wrapper.append(e.titleBar);
8599
}
86100
87101
{ /* Mimetype... */
@@ -191,17 +205,19 @@
191205
e.tabEdit.append(e.editor);
192206
e.tabEdit.dataset.tabLabel = 'Edit';
193207
this.#tabs.addTab( e.tabEdit );
194208
this.#tabs.switchToTab( e.tabEdit );
195209
if( this.#draft ){
196
- this.editorContent = this.#draft.content || '';
210
+ this.editorContent = opt.edit?.W || this.#draft.content || '';
197211
e.editor.addEventListener(
198212
'blur', ()=>{
199213
this.#draft.content = this.editorContent;
200214
this.#storeDraft();
201215
}
202216
);
217
+ }else if( opt.edit?.W ){
218
+ this.editorContent = opt.artifact.W;
203219
}
204220
e.preview = D.addClass(D.div(), 'preview');
205221
e.preview.dataset.tabLabel = 'Preview';
206222
this.#tabs.addTab( e.preview );
207223
}
@@ -472,11 +488,15 @@
472488
async #fetchPreview(content){
473489
/* TODO: fetch preview */
474490
const e = this.#e;
475491
const fd = this.#newFormData(content);
476492
return window
477
- .fetch(F.repoUrl('wikiajax/preview'), {
493
+ .fetch(F.repoUrl(
494
+ 'wikiajax/preview'
495
+ /* ^^^ Maybe change to /ajax/preview-text, but it's
496
+ ** got a more complicated interface */
497
+ ), {
478498
method: 'POST',
479499
body: fd
480500
})
481501
.then(r=>r.text())
482502
.then(t=>{
@@ -799,10 +819,11 @@
799819
});
800820
});
801821
});
802822
}
803823
824
+ const userIsIndividual = ['anonymous','guest'].indexOf(F.user.name)<0;
804825
const eForumNew = document.body.classList.contains('cpage-forumnew')
805826
? document.querySelector('#forumnew-placeholder')
806827
: null;
807828
if( eForumNew ){
808829
/* /forumnew */
@@ -817,42 +838,116 @@
817838
});
818839
eForumNew.parentElement.insertBefore(fpe.widget, eForumNew);
819840
eForumNew.remove();
820841
fossil.page.fpe = fpe /* for testing via the console */;
821842
}/*eForumNew*/
822
- else if( 0 && (document.body.classList.contains('cpage-forumpost')
823
- || document.body.classList.contains('cpage-forumthread'))){
843
+ else if( userIsIndividual
844
+ && (document.body.classList.contains('cpage-forumpost')
845
+ || document.body.classList.contains('cpage-forumthread'))){
824846
/* /forumpost and /forumthread */
825
- const replyClicked = (replyButton, fpid)=>{
847
+
848
+ const fetchPost = async (fpid)=>{
849
+ return window.fetch(F.repoUrl('ajax/artifact.json?uuid='+fpid))
850
+ .then(r=>r.json())
851
+ .then(j=>{
852
+ if( j.error ) throw new Error(j.error);
853
+ return j;
854
+ });
855
+ };
856
+
857
+ const setupEditReplyElement = (ePost, eButton)=>{
858
+ const fpid = ePost.dataset.fpid;
859
+ ePost.dataset.originalMarginLeft = ePost.style.marginLeft;
860
+ ePost.style.marginLeft = 'initial';
861
+ eButton.disabled = true;
862
+ eButton.dataset.originalLabel = eButton.value;
863
+ return fpid;
864
+ };
865
+
866
+ const restoreEditReplyElement = (ePost, eButton)=>{
867
+ if( ePost.dataset.originalMarginLeft ){
868
+ ePost.style.marginLeft = ePost.dataset.originalMarginLeft;
869
+ delete ePost.dataset.originalMarginLeft;
870
+ }
871
+ eButton.disabled = false;
872
+ if( eButton.dataset.originalLabel ){
873
+ eButton.innerText = eButton.dataset.originalLabel;
874
+ delete eButton.dataset.originalLabel;
875
+ }
876
+ };
877
+
878
+ const replyClicked = (ePost, eBtnReply)=>{
879
+ const fpid = setupEditReplyElement(ePost, eBtnReply);
880
+ eBtnReply.innerText = "Replying...";
826881
F.toast.error("Reply is TODO. fpid="+fpid);
827882
/*
828883
TODOs include:
829884
830885
- Hide replyButton
831886
832
- - Pop up a ForumPostEditor. It needs a Cancel button.
887
+ - Shift ePost to the left edge.
888
+
889
+ - Fetch /ajax/artifact.json?uuid=fpid
890
+
891
+ - Pop up a ForumPostEditor immediately under ePost. It needs
892
+ a Cancel button.
893
+
894
+ - When cancelled or submitted, restore the reply button and
895
+ ePost position.
896
+ */
897
+ if( 0 ) restoreEditReplyElement(ePost, eBtnReply);
898
+ }/*replyClicked()*/;
899
+
900
+ const editClicked = (ePost, eBtnEdit)=>{
901
+ const fpid = setupEditReplyElement(ePost, eBtnEdit);
902
+ eBtnEdit.innerText = "Editing...";
903
+ F.toast.error("Edit is TODO. fpid="+fpid);
904
+ /*
905
+ TODOs include:
906
+
907
+ - Disable editButton.
908
+
909
+ - Shift ePost to the left edge.
910
+
911
+ - Pop up a ForumPostEditor immediately under ePost. It needs
912
+ a Cancel button.
833913
834
- - When cancelled or submitted, restore the reply button.
914
+ - When cancelled or submitted, restore the edit button and
915
+ ePost position.
835916
*/
836
- };
917
+ fetchPost(fpid)
918
+ .then(j=>{
919
+ console.debug("Got post... now what?", j);
920
+ if( 0 ) restoreEditReplyElement(ePost, eBtnEdit);
921
+ });
922
+ }/*editClicked()*/;
923
+
837924
document.body.querySelectorAll(
838925
'.forumpost-single-controls > form'
839926
).forEach(form=>{
840
- const eReplyTo = form.parentElement.parentElement;
841
- const fpid = eReplyTo?.dataset?.fpid;
842
- if( !fpid ){
843
- console.warn("Unexpected non-fpid", form, eReplyTo);
927
+ //console.debug("Checking form",form);
928
+ const eThePost = form.parentElement.parentElement;
929
+ if( !eThePost?.dataset?.fpid ){
930
+ console.warn("Unexpected missing fpid", eThePost);
844931
return;
845932
}
846
- const rb = form.querySelector('input[type=submit][name=reply]');
847
- if( rb ){
848
- console.debug("hacking Reply button", rb);
849
- const b = D.button("Reply", ()=>replyClicked(b, fpid));
933
+ const btnReply = form.querySelector('input[type=submit][name=reply]');
934
+ if( btnReply ){
935
+ //console.debug("hacking Reply button", btnReply);
936
+ const b = D.button("Reply", ()=>replyClicked(eThePost, b));
850937
b.type = 'button'/*keep container form from submitting*/;
851
- rb.parentElement.insertBefore(b, rb);
852
- rb.remove();
938
+ btnReply.parentElement.insertBefore(b, btnReply);
939
+ btnReply.remove();
940
+ }
941
+ const btnEdit = form.querySelector('input[type=submit][name=edit]');
942
+ if( btnEdit ){
943
+ //console.debug("hacking Edit button", btnEdit);
944
+ const b = D.button("Edit", ()=>editClicked(eThePost, b));
945
+ b.type = 'button';
946
+ btnEdit.parentElement.insertBefore(b, btnEdit);
947
+ btnEdit.remove();
853948
}
854
- });
855
- }
949
+ })/*for-each form*/;
950
+ }/* /forumpost and /forumthread */
856951
857952
})/*F.onPageLoad callback*/;
858953
})(window.fossil);
859954
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -38,19 +38,33 @@
38
39 opt.draftKey[string=undefined]: if set then this object's state
40 will be stored in fossil.storage when the relevant input fields
41 lose focus. If old state is found, the form is pre-populated
42 from it. The state is cleared on a successful submit.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43 */
44 constructor(opt){
45 opt = this.#opt = F.nu({
46 // todo: defaults once we determine the options
47 // replyTo: hash
48 // fpid: hash
49 draftKey: undefined
50 }, opt);
51 opt.isNewThread = !opt.replyTo && !opt.edit;
52 if( opt.draftKey ){
53 this.#draft = F.nu(F.storage.getJSON(opt.draftKey, {}));
54 }
55 const e = this.#e = F.nu({
56 mimetype: F.nu(),
@@ -75,13 +89,13 @@
75 if( this.#draft ){
76 e.title.addEventListener('blur', ()=>{
77 this.#draft.title = e.title.value;
78 this.#storeDraft();
79 });
80 e.title.value = opt.title || this.#draft.title || '';
81 }else if( opt.title ){
82 e.title.value = opt.title;
83 }
84 wrapper.append(e.titleBar);
85 }
86
87 { /* Mimetype... */
@@ -191,17 +205,19 @@
191 e.tabEdit.append(e.editor);
192 e.tabEdit.dataset.tabLabel = 'Edit';
193 this.#tabs.addTab( e.tabEdit );
194 this.#tabs.switchToTab( e.tabEdit );
195 if( this.#draft ){
196 this.editorContent = this.#draft.content || '';
197 e.editor.addEventListener(
198 'blur', ()=>{
199 this.#draft.content = this.editorContent;
200 this.#storeDraft();
201 }
202 );
 
 
203 }
204 e.preview = D.addClass(D.div(), 'preview');
205 e.preview.dataset.tabLabel = 'Preview';
206 this.#tabs.addTab( e.preview );
207 }
@@ -472,11 +488,15 @@
472 async #fetchPreview(content){
473 /* TODO: fetch preview */
474 const e = this.#e;
475 const fd = this.#newFormData(content);
476 return window
477 .fetch(F.repoUrl('wikiajax/preview'), {
 
 
 
 
478 method: 'POST',
479 body: fd
480 })
481 .then(r=>r.text())
482 .then(t=>{
@@ -799,10 +819,11 @@
799 });
800 });
801 });
802 }
803
 
804 const eForumNew = document.body.classList.contains('cpage-forumnew')
805 ? document.querySelector('#forumnew-placeholder')
806 : null;
807 if( eForumNew ){
808 /* /forumnew */
@@ -817,42 +838,116 @@
817 });
818 eForumNew.parentElement.insertBefore(fpe.widget, eForumNew);
819 eForumNew.remove();
820 fossil.page.fpe = fpe /* for testing via the console */;
821 }/*eForumNew*/
822 else if( 0 && (document.body.classList.contains('cpage-forumpost')
823 || document.body.classList.contains('cpage-forumthread'))){
 
824 /* /forumpost and /forumthread */
825 const replyClicked = (replyButton, fpid)=>{
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
826 F.toast.error("Reply is TODO. fpid="+fpid);
827 /*
828 TODOs include:
829
830 - Hide replyButton
831
832 - Pop up a ForumPostEditor. It needs a Cancel button.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
833
834 - When cancelled or submitted, restore the reply button.
 
835 */
836 };
 
 
 
 
 
 
837 document.body.querySelectorAll(
838 '.forumpost-single-controls > form'
839 ).forEach(form=>{
840 const eReplyTo = form.parentElement.parentElement;
841 const fpid = eReplyTo?.dataset?.fpid;
842 if( !fpid ){
843 console.warn("Unexpected non-fpid", form, eReplyTo);
844 return;
845 }
846 const rb = form.querySelector('input[type=submit][name=reply]');
847 if( rb ){
848 console.debug("hacking Reply button", rb);
849 const b = D.button("Reply", ()=>replyClicked(b, fpid));
850 b.type = 'button'/*keep container form from submitting*/;
851 rb.parentElement.insertBefore(b, rb);
852 rb.remove();
 
 
 
 
 
 
 
 
853 }
854 });
855 }
856
857 })/*F.onPageLoad callback*/;
858 })(window.fossil);
859
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -38,19 +38,33 @@
38
39 opt.draftKey[string=undefined]: if set then this object's state
40 will be stored in fossil.storage when the relevant input fields
41 lose focus. If old state is found, the form is pre-populated
42 from it. The state is cleared on a successful submit.
43
44 opt.ondiscard[=function]: if set, a Discard button is added
45 which, when activated, clears the current draft and removes
46 this object's widget from the DOM. After that, opt.ondiscard()
47 is called and passed no arguments.
48
49 TODO:
50
51 opt.inReplyTo=uuid: if this is a new response to a post, this
52 is the full forum post uuid of the being-replied-to post.
53
54 opt.edit=artifactObject: if this is an edit of an existing
55 post, this is the full JSON-format artifact of the forum post
56 the being-edit post.
57 */
58 constructor(opt){
59 opt = this.#opt = F.nu({
60 // todo: defaults once we determine the options
61 // inReplyTo: hash
62 // fpid: hash
63 draftKey: undefined
64 }, opt);
65 opt.isNewThread = !opt.inReplyTo && !opt.edit;
66 if( opt.draftKey ){
67 this.#draft = F.nu(F.storage.getJSON(opt.draftKey, {}));
68 }
69 const e = this.#e = F.nu({
70 mimetype: F.nu(),
@@ -75,13 +89,13 @@
89 if( this.#draft ){
90 e.title.addEventListener('blur', ()=>{
91 this.#draft.title = e.title.value;
92 this.#storeDraft();
93 });
94 e.title.value = opt.edit?.H || this.#draft.title || '';
95 }else if( opt.edit?.H ){
96 e.title.value = opt.edit.H;
97 }
98 wrapper.append(e.titleBar);
99 }
100
101 { /* Mimetype... */
@@ -191,17 +205,19 @@
205 e.tabEdit.append(e.editor);
206 e.tabEdit.dataset.tabLabel = 'Edit';
207 this.#tabs.addTab( e.tabEdit );
208 this.#tabs.switchToTab( e.tabEdit );
209 if( this.#draft ){
210 this.editorContent = opt.edit?.W || this.#draft.content || '';
211 e.editor.addEventListener(
212 'blur', ()=>{
213 this.#draft.content = this.editorContent;
214 this.#storeDraft();
215 }
216 );
217 }else if( opt.edit?.W ){
218 this.editorContent = opt.artifact.W;
219 }
220 e.preview = D.addClass(D.div(), 'preview');
221 e.preview.dataset.tabLabel = 'Preview';
222 this.#tabs.addTab( e.preview );
223 }
@@ -472,11 +488,15 @@
488 async #fetchPreview(content){
489 /* TODO: fetch preview */
490 const e = this.#e;
491 const fd = this.#newFormData(content);
492 return window
493 .fetch(F.repoUrl(
494 'wikiajax/preview'
495 /* ^^^ Maybe change to /ajax/preview-text, but it's
496 ** got a more complicated interface */
497 ), {
498 method: 'POST',
499 body: fd
500 })
501 .then(r=>r.text())
502 .then(t=>{
@@ -799,10 +819,11 @@
819 });
820 });
821 });
822 }
823
824 const userIsIndividual = ['anonymous','guest'].indexOf(F.user.name)<0;
825 const eForumNew = document.body.classList.contains('cpage-forumnew')
826 ? document.querySelector('#forumnew-placeholder')
827 : null;
828 if( eForumNew ){
829 /* /forumnew */
@@ -817,42 +838,116 @@
838 });
839 eForumNew.parentElement.insertBefore(fpe.widget, eForumNew);
840 eForumNew.remove();
841 fossil.page.fpe = fpe /* for testing via the console */;
842 }/*eForumNew*/
843 else if( userIsIndividual
844 && (document.body.classList.contains('cpage-forumpost')
845 || document.body.classList.contains('cpage-forumthread'))){
846 /* /forumpost and /forumthread */
847
848 const fetchPost = async (fpid)=>{
849 return window.fetch(F.repoUrl('ajax/artifact.json?uuid='+fpid))
850 .then(r=>r.json())
851 .then(j=>{
852 if( j.error ) throw new Error(j.error);
853 return j;
854 });
855 };
856
857 const setupEditReplyElement = (ePost, eButton)=>{
858 const fpid = ePost.dataset.fpid;
859 ePost.dataset.originalMarginLeft = ePost.style.marginLeft;
860 ePost.style.marginLeft = 'initial';
861 eButton.disabled = true;
862 eButton.dataset.originalLabel = eButton.value;
863 return fpid;
864 };
865
866 const restoreEditReplyElement = (ePost, eButton)=>{
867 if( ePost.dataset.originalMarginLeft ){
868 ePost.style.marginLeft = ePost.dataset.originalMarginLeft;
869 delete ePost.dataset.originalMarginLeft;
870 }
871 eButton.disabled = false;
872 if( eButton.dataset.originalLabel ){
873 eButton.innerText = eButton.dataset.originalLabel;
874 delete eButton.dataset.originalLabel;
875 }
876 };
877
878 const replyClicked = (ePost, eBtnReply)=>{
879 const fpid = setupEditReplyElement(ePost, eBtnReply);
880 eBtnReply.innerText = "Replying...";
881 F.toast.error("Reply is TODO. fpid="+fpid);
882 /*
883 TODOs include:
884
885 - Hide replyButton
886
887 - Shift ePost to the left edge.
888
889 - Fetch /ajax/artifact.json?uuid=fpid
890
891 - Pop up a ForumPostEditor immediately under ePost. It needs
892 a Cancel button.
893
894 - When cancelled or submitted, restore the reply button and
895 ePost position.
896 */
897 if( 0 ) restoreEditReplyElement(ePost, eBtnReply);
898 }/*replyClicked()*/;
899
900 const editClicked = (ePost, eBtnEdit)=>{
901 const fpid = setupEditReplyElement(ePost, eBtnEdit);
902 eBtnEdit.innerText = "Editing...";
903 F.toast.error("Edit is TODO. fpid="+fpid);
904 /*
905 TODOs include:
906
907 - Disable editButton.
908
909 - Shift ePost to the left edge.
910
911 - Pop up a ForumPostEditor immediately under ePost. It needs
912 a Cancel button.
913
914 - When cancelled or submitted, restore the edit button and
915 ePost position.
916 */
917 fetchPost(fpid)
918 .then(j=>{
919 console.debug("Got post... now what?", j);
920 if( 0 ) restoreEditReplyElement(ePost, eBtnEdit);
921 });
922 }/*editClicked()*/;
923
924 document.body.querySelectorAll(
925 '.forumpost-single-controls > form'
926 ).forEach(form=>{
927 //console.debug("Checking form",form);
928 const eThePost = form.parentElement.parentElement;
929 if( !eThePost?.dataset?.fpid ){
930 console.warn("Unexpected missing fpid", eThePost);
931 return;
932 }
933 const btnReply = form.querySelector('input[type=submit][name=reply]');
934 if( btnReply ){
935 //console.debug("hacking Reply button", btnReply);
936 const b = D.button("Reply", ()=>replyClicked(eThePost, b));
937 b.type = 'button'/*keep container form from submitting*/;
938 btnReply.parentElement.insertBefore(b, btnReply);
939 btnReply.remove();
940 }
941 const btnEdit = form.querySelector('input[type=submit][name=edit]');
942 if( btnEdit ){
943 //console.debug("hacking Edit button", btnEdit);
944 const b = D.button("Edit", ()=>editClicked(eThePost, b));
945 b.type = 'button';
946 btnEdit.parentElement.insertBefore(b, btnEdit);
947 btnEdit.remove();
948 }
949 })/*for-each form*/;
950 }/* /forumpost and /forumthread */
951
952 })/*F.onPageLoad callback*/;
953 })(window.fossil);
954

Keyboard Shortcuts

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