Fossil SCM

Have /forumnew use the old editor form only in no-js environments, else the new form. Hook preview rendering up to the new form.

stephan 2026-06-06 13:59 UTC forum-editor-2026
Commit 3addcc0abdf6b8e010eb3ddf50d0fc92fd1860cfe2913d5797757a605d65d970
+11
--- src/forum.c
+++ src/forum.c
@@ -1997,10 +1997,12 @@
19971997
@ <h1>Preview:</h1>
19981998
forum_render(zTitle, zMimetype, zContent, "forumEdit", 1);
19991999
}
20002000
style_set_current_feature("forum");
20012001
style_header("New Forum Thread");
2002
+
2003
+ @ <noscript>
20022004
@ <form action="%R/forume1" method="POST">
20032005
@ <h1>New Thread:</h1>
20042006
forum_from_line();
20052007
forum_post_widget(zTitle, zMimetype, zContent);
20062008
@ <input type="submit" name="preview" value="Preview">
@@ -2011,10 +2013,19 @@
20112013
}
20122014
forum_render_debug_options();
20132015
login_insert_csrf_secret();
20142016
@ </form>
20152017
forum_render_attachment_notice();
2018
+ @ </noscript>
2019
+ /* When JS is disabled the block above will work.
2020
+ When it's enabled, the above won't do anything and
2021
+ JS will render the editor form. */
2022
+
2023
+ @ <div hidden id='forumnew-placeholder'>
2024
+ @ <input type='hidden' name='title' value='%h(zTitle)'>
2025
+ login_insert_csrf_secret() /* the 2026 form */;
2026
+ @ </div>
20162027
forum_emit_js();
20172028
style_finish_page();
20182029
}
20192030
20202031
/*
20212032
--- src/forum.c
+++ src/forum.c
@@ -1997,10 +1997,12 @@
1997 @ <h1>Preview:</h1>
1998 forum_render(zTitle, zMimetype, zContent, "forumEdit", 1);
1999 }
2000 style_set_current_feature("forum");
2001 style_header("New Forum Thread");
 
 
2002 @ <form action="%R/forume1" method="POST">
2003 @ <h1>New Thread:</h1>
2004 forum_from_line();
2005 forum_post_widget(zTitle, zMimetype, zContent);
2006 @ <input type="submit" name="preview" value="Preview">
@@ -2011,10 +2013,19 @@
2011 }
2012 forum_render_debug_options();
2013 login_insert_csrf_secret();
2014 @ </form>
2015 forum_render_attachment_notice();
 
 
 
 
 
 
 
 
 
2016 forum_emit_js();
2017 style_finish_page();
2018 }
2019
2020 /*
2021
--- src/forum.c
+++ src/forum.c
@@ -1997,10 +1997,12 @@
1997 @ <h1>Preview:</h1>
1998 forum_render(zTitle, zMimetype, zContent, "forumEdit", 1);
1999 }
2000 style_set_current_feature("forum");
2001 style_header("New Forum Thread");
2002
2003 @ <noscript>
2004 @ <form action="%R/forume1" method="POST">
2005 @ <h1>New Thread:</h1>
2006 forum_from_line();
2007 forum_post_widget(zTitle, zMimetype, zContent);
2008 @ <input type="submit" name="preview" value="Preview">
@@ -2011,10 +2013,19 @@
2013 }
2014 forum_render_debug_options();
2015 login_insert_csrf_secret();
2016 @ </form>
2017 forum_render_attachment_notice();
2018 @ </noscript>
2019 /* When JS is disabled the block above will work.
2020 When it's enabled, the above won't do anything and
2021 JS will render the editor form. */
2022
2023 @ <div hidden id='forumnew-placeholder'>
2024 @ <input type='hidden' name='title' value='%h(zTitle)'>
2025 login_insert_csrf_secret() /* the 2026 form */;
2026 @ </div>
2027 forum_emit_js();
2028 style_finish_page();
2029 }
2030
2031 /*
2032
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -315,17 +315,23 @@
315315
eDropzone.classList.add('dragover');
316316
});
317317
eDropzone.addEventListener('dragleave', (ev)=>{
318318
eDropzone.classList.remove('dragover');
319319
});
320
- eDropzone.addEventListener('drop', (ev)=>{
320
+ const handleDrop = (ev, theRealRowObj)=>{
321321
ev.preventDefault();
322322
eDropzone.classList.remove('dragover');
323323
if( ev.dataTransfer.files.length ){
324
- this.#injestBlob(rowObj, ev.dataTransfer.files[0]);
324
+ const r = theRealRowObj || rowObj;
325
+ this.#injestBlob(r, ev.dataTransfer.files[0]);
325326
}
326
- });
327
+ };
328
+ /* Isn't working? eBtnAdd.addEventListener('drop', (ev)=>{
329
+ this.#addRow();
330
+ handleDrop(ev, this.#rows[this.#rows.length-1]);
331
+ });*/
332
+ eDropzone.addEventListener('drop', handleDrop);
327333
const pasteImage = (event, item)=>{
328334
if( item.type.indexOf('image') === 0 ) {
329335
event.preventDefault();
330336
const blob = item.getAsFile();
331337
if( blob.name === 'image.png' ){
332338
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -315,17 +315,23 @@
315 eDropzone.classList.add('dragover');
316 });
317 eDropzone.addEventListener('dragleave', (ev)=>{
318 eDropzone.classList.remove('dragover');
319 });
320 eDropzone.addEventListener('drop', (ev)=>{
321 ev.preventDefault();
322 eDropzone.classList.remove('dragover');
323 if( ev.dataTransfer.files.length ){
324 this.#injestBlob(rowObj, ev.dataTransfer.files[0]);
 
325 }
326 });
 
 
 
 
 
327 const pasteImage = (event, item)=>{
328 if( item.type.indexOf('image') === 0 ) {
329 event.preventDefault();
330 const blob = item.getAsFile();
331 if( blob.name === 'image.png' ){
332
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -315,17 +315,23 @@
315 eDropzone.classList.add('dragover');
316 });
317 eDropzone.addEventListener('dragleave', (ev)=>{
318 eDropzone.classList.remove('dragover');
319 });
320 const handleDrop = (ev, theRealRowObj)=>{
321 ev.preventDefault();
322 eDropzone.classList.remove('dragover');
323 if( ev.dataTransfer.files.length ){
324 const r = theRealRowObj || rowObj;
325 this.#injestBlob(r, ev.dataTransfer.files[0]);
326 }
327 };
328 /* Isn't working? eBtnAdd.addEventListener('drop', (ev)=>{
329 this.#addRow();
330 handleDrop(ev, this.#rows[this.#rows.length-1]);
331 });*/
332 eDropzone.addEventListener('drop', handleDrop);
333 const pasteImage = (event, item)=>{
334 if( item.type.indexOf('image') === 0 ) {
335 event.preventDefault();
336 const blob = item.getAsFile();
337 if( blob.name === 'image.png' ){
338
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -25,34 +25,42 @@
2525
#tabs;
2626
/* Elements to disable while an XHR is pending. */
2727
#toDisable = [];
2828
/* DOM element of the current active tab. */
2929
#activeTab;
30
+ /* Extra input[type=hidden] fields imported from fossil's
31
+ static page generation. */
32
+ #extraFields;
3033
34
+ /**
35
+ */
3136
constructor(opt){
3237
opt = this.#opt = F.nu({
3338
// todo: defaults once we determine the options
3439
// replyTo: hash
3540
// edit: hash
3641
}, opt);
42
+ opt.isNewThread = !opt.replyTo && !opt.edit;
3743
const e = this.#e = F.nu({
3844
mimetype: F.nu(),
3945
button: F.nu()
4046
});
4147
const wrapper = e.widget = D.addClass(D.div(), 'ForumPostEditor');
4248
D.clearElement(wrapper);
4349
if( !opt.inReplyTo ){
4450
e.titleBar = D.addClass(D.div(),'titlebar');
4551
e.title = D.addClass(D.input('text'), 'title');
52
+ e.title.setAttribute('maxlength', 125);
4653
e.titleBar.append(
4754
D.append(D.span(), "Title:"),
4855
e.title
4956
);
5057
wrapper.append(e.titleBar);
5158
}
5259
e.mimetype.wrapper = D.addClass(D.div(), 'mimetype-wrapper');
5360
e.mimetype.select = D.addClass(D.select(), 'mimetype-select');
61
+ this.#toDisable.push(e.mimetype.select);
5462
e.mimetype.label = D.span();
5563
e.mimetype.label.append(
5664
D.a(F.repoUrl('markup_help'), 'Markup style'),
5765
':'
5866
);
@@ -67,14 +75,18 @@
6775
if( !i++ ) o.setAttribute('selected', '');
6876
}
6977
7078
e.button.preview = D.button("Preview", e=>this.#preview());
7179
e.button.submit = D.button("Submit");
72
- F.confirmer(e.button.submit, {
73
- confirmText: "Confirm submit...",
74
- onconfirm: ()=>this.#submit()
75
- });
80
+ if( 1 ){
81
+ F.confirmer(e.button.submit, {
82
+ confirmText: "Confirm submit...",
83
+ onconfirm: ()=>this.#submit()
84
+ });
85
+ }else{
86
+ e.button.submit.addEventListener('click', ()=>this.#submit());
87
+ }
7688
e.button.submit.setAttribute('disabled', '');
7789
e.buttons = D.addClass(D.div(), 'buttons');
7890
wrapper.append(e.buttons);
7991
8092
e.err = D.addClass(D.div(), 'error', 'hidden');
@@ -86,32 +98,33 @@
8698
e.tabs = D.attr(
8799
D.addClass(D.div(), 'tab-container'),
88100
'id', idPrefix+'-tabs'
89101
);
90102
this.#tabs = new F.TabManager(e.tabs);
103
+ this.#tabs.addEventListener('before-switch-to', (ev)=>{
104
+ this.#activeTab = ev.detail;
105
+ if( e.preview === this.#activeTab ){
106
+ this.#e.button.preview.click();
107
+ }
108
+ });
91109
wrapper.append( e.tabs );
92110
93111
e.tabEdit = D.div();
94112
e.tabEdit.classList.add('editor-wrapper');
95113
e.editor = D.attr(
96114
D.addClass(D.textarea(), 'editor'),
97115
'placeholder',
98
- 'Your content...'
116
+ 'Your message to other forum-goers...'
99117
);
100118
e.tabEdit.append(e.editor);
101119
e.tabEdit.dataset.tabLabel = 'Edit';
102120
this.#tabs.addTab( e.tabEdit );
121
+ this.#tabs.switchToTab( e.tabEdit );
103122
104123
e.preview = D.addClass(D.div(), 'preview');
105124
e.preview.dataset.tabLabel = 'Preview';
106125
this.#tabs.addTab( e.preview );
107
- this.#tabs.addEventListener('before-switch-to', (ev)=>{
108
- this.#activeTab = ev.detail;
109
- if( e.preview === this.#activeTab ){
110
- this.#e.button.preview.click();
111
- }
112
- });
113126
}
114127
115128
if( F.user.enableDebug ){
116129
e.debug = D.addClass(D.div(), 'debug');
117130
e.debug.dataset.tabLabel = 'Debug';
@@ -147,11 +160,10 @@
147160
148161
get widget(){
149162
return this.#e.widget;
150163
}
151164
152
-
153165
/**
154166
Reports an error by appending each argument to the error widget
155167
and unhiding it. If passed no arugments, it clears and hides
156168
the error widget.
157169
*/
@@ -164,14 +176,48 @@
164176
}else{
165177
e.classList.add('hidden');
166178
}
167179
}
168180
169
- async #fetchPreview(){
181
+ /**
182
+ Adds a list of input[type=hidden] form fields to this object,
183
+ imported from the server-generated HTML. This is used for collecting,
184
+ e.g., the CSRF token.
185
+ */
186
+ addHiddenFields(list){
187
+ this.#extraFields ??= [];
188
+ for( const f of list ){
189
+ if( 'title'===f.name && this.#opt.isNewThread ){
190
+ this.#e.title.value = f.value;
191
+ }else{
192
+ this.#extraFields.push(f);
193
+ }
194
+ }
195
+ }
196
+
197
+ async #fetchPreview(content){
170198
/* TODO: fetch preview */
171
- this.#isWaiting = false;
172
- D.enable(this.#toDisable);
199
+ const e = this.#e;
200
+ const fd = new FormData;
201
+ for(const f of this.#extraFields){
202
+ fd.append(f.name, f.value);
203
+ }
204
+ fd.append('mimetype', e.mimetype.select.value);
205
+ fd.append('content', content);
206
+ return window
207
+ .fetch(F.repoUrl('wikiajax/preview'), {
208
+ method: 'POST',
209
+ body: fd
210
+ })
211
+ .then(r=>r.text())
212
+ .then(t=>{
213
+ if( /^\{.*}$/.test(t) ){
214
+ const o = JSON.parse(t);
215
+ throw new Error(o.error);
216
+ }
217
+ return t;
218
+ });
173219
}
174220
175221
async #preview(){
176222
if( this.#isWaiting ) return;
177223
const e = this.#e;
@@ -179,28 +225,49 @@
179225
this.#tabs.switchToTab(e.preview);
180226
/* Will recurse into here */
181227
return;
182228
}
183229
this.#isWaiting = true;
230
+ D.clearElement(e.preview);
231
+ const content = e.editor.value.trim();
232
+ if( !content ){
233
+ return;
234
+ }
184235
D.disable(this.#toDisable, e.button.submit);
185236
e.preview.textContent = "Fetching preview...";
186
- this.#fetchPreview()
187
- .then(()=>{
188
- e.preview.textContent = "TODO: actually fetch the preview "+Date.now();
237
+ this.#fetchPreview(content)
238
+ .then((c)=>{
239
+ e.preview.innerHTML = c;
189240
D.enable(this.#toDisable, e.button.submit);
190241
})
191
- .catch(e=>{
192
- this.reportError(e.message);
242
+ .catch(err=>{
243
+ e.preview.textContent = "Error fetching preview: "+err.message;
244
+ this.reportError(err.message);
245
+ })
246
+ .finally(()=>{
247
+ this.#isWaiting = false;
193248
D.enable(this.#toDisable);
249
+ console.warn("finally()!");
194250
});
195251
}
252
+
253
+ #validate(){
254
+ let v = this.#e.title.value.trim();
255
+ if( !v ){
256
+ this.reportError("A non-empty title is required.");
257
+ return;
258
+ }
259
+ return true;
260
+ }
196261
197262
#submit(){
198263
if( this.#isWaiting ) return;
264
+ if( !this.#validate() ) return;
199265
this.#isWaiting = true;
200266
const e = this.#e;
201267
D.disable(e.button.submit);
268
+ this.reportError("Submit is TODO.");
202269
/*
203270
TODO: save it, set #isWaiting=false, then handle error or
204271
redirect to the post (if this is a new post) or, if replying
205272
inline, replace this object with a static rendering from the
206273
response.
@@ -391,7 +458,18 @@
391458
});
392459
});
393460
});
394461
}
395462
463
+ const eForumNew = document.body.classList.contains('cpage-forumnew')
464
+ ? document.querySelector('#forumnew-placeholder')
465
+ : null;
466
+ if( eForumNew ){
467
+ /* /forumnew */
468
+ const fpe = new fossil.ForumPostEditor({});
469
+ fpe.addHiddenFields( eForumNew.querySelectorAll('input[type=hidden]') );
470
+ eForumNew.parentElement.insertBefore(fpe.widget, eForumNew);
471
+ eForumNew.remove();
472
+ fossil.page.fpe = fpe /* for testing via the console */;
473
+ }/*eForumNew*/
396474
})/*F.onPageLoad callback*/;
397475
})(window.fossil);
398476
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -25,34 +25,42 @@
25 #tabs;
26 /* Elements to disable while an XHR is pending. */
27 #toDisable = [];
28 /* DOM element of the current active tab. */
29 #activeTab;
 
 
 
30
 
 
31 constructor(opt){
32 opt = this.#opt = F.nu({
33 // todo: defaults once we determine the options
34 // replyTo: hash
35 // edit: hash
36 }, opt);
 
37 const e = this.#e = F.nu({
38 mimetype: F.nu(),
39 button: F.nu()
40 });
41 const wrapper = e.widget = D.addClass(D.div(), 'ForumPostEditor');
42 D.clearElement(wrapper);
43 if( !opt.inReplyTo ){
44 e.titleBar = D.addClass(D.div(),'titlebar');
45 e.title = D.addClass(D.input('text'), 'title');
 
46 e.titleBar.append(
47 D.append(D.span(), "Title:"),
48 e.title
49 );
50 wrapper.append(e.titleBar);
51 }
52 e.mimetype.wrapper = D.addClass(D.div(), 'mimetype-wrapper');
53 e.mimetype.select = D.addClass(D.select(), 'mimetype-select');
 
54 e.mimetype.label = D.span();
55 e.mimetype.label.append(
56 D.a(F.repoUrl('markup_help'), 'Markup style'),
57 ':'
58 );
@@ -67,14 +75,18 @@
67 if( !i++ ) o.setAttribute('selected', '');
68 }
69
70 e.button.preview = D.button("Preview", e=>this.#preview());
71 e.button.submit = D.button("Submit");
72 F.confirmer(e.button.submit, {
73 confirmText: "Confirm submit...",
74 onconfirm: ()=>this.#submit()
75 });
 
 
 
 
76 e.button.submit.setAttribute('disabled', '');
77 e.buttons = D.addClass(D.div(), 'buttons');
78 wrapper.append(e.buttons);
79
80 e.err = D.addClass(D.div(), 'error', 'hidden');
@@ -86,32 +98,33 @@
86 e.tabs = D.attr(
87 D.addClass(D.div(), 'tab-container'),
88 'id', idPrefix+'-tabs'
89 );
90 this.#tabs = new F.TabManager(e.tabs);
 
 
 
 
 
 
91 wrapper.append( e.tabs );
92
93 e.tabEdit = D.div();
94 e.tabEdit.classList.add('editor-wrapper');
95 e.editor = D.attr(
96 D.addClass(D.textarea(), 'editor'),
97 'placeholder',
98 'Your content...'
99 );
100 e.tabEdit.append(e.editor);
101 e.tabEdit.dataset.tabLabel = 'Edit';
102 this.#tabs.addTab( e.tabEdit );
 
103
104 e.preview = D.addClass(D.div(), 'preview');
105 e.preview.dataset.tabLabel = 'Preview';
106 this.#tabs.addTab( e.preview );
107 this.#tabs.addEventListener('before-switch-to', (ev)=>{
108 this.#activeTab = ev.detail;
109 if( e.preview === this.#activeTab ){
110 this.#e.button.preview.click();
111 }
112 });
113 }
114
115 if( F.user.enableDebug ){
116 e.debug = D.addClass(D.div(), 'debug');
117 e.debug.dataset.tabLabel = 'Debug';
@@ -147,11 +160,10 @@
147
148 get widget(){
149 return this.#e.widget;
150 }
151
152
153 /**
154 Reports an error by appending each argument to the error widget
155 and unhiding it. If passed no arugments, it clears and hides
156 the error widget.
157 */
@@ -164,14 +176,48 @@
164 }else{
165 e.classList.add('hidden');
166 }
167 }
168
169 async #fetchPreview(){
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170 /* TODO: fetch preview */
171 this.#isWaiting = false;
172 D.enable(this.#toDisable);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173 }
174
175 async #preview(){
176 if( this.#isWaiting ) return;
177 const e = this.#e;
@@ -179,28 +225,49 @@
179 this.#tabs.switchToTab(e.preview);
180 /* Will recurse into here */
181 return;
182 }
183 this.#isWaiting = true;
 
 
 
 
 
184 D.disable(this.#toDisable, e.button.submit);
185 e.preview.textContent = "Fetching preview...";
186 this.#fetchPreview()
187 .then(()=>{
188 e.preview.textContent = "TODO: actually fetch the preview "+Date.now();
189 D.enable(this.#toDisable, e.button.submit);
190 })
191 .catch(e=>{
192 this.reportError(e.message);
 
 
 
 
193 D.enable(this.#toDisable);
 
194 });
195 }
 
 
 
 
 
 
 
 
 
196
197 #submit(){
198 if( this.#isWaiting ) return;
 
199 this.#isWaiting = true;
200 const e = this.#e;
201 D.disable(e.button.submit);
 
202 /*
203 TODO: save it, set #isWaiting=false, then handle error or
204 redirect to the post (if this is a new post) or, if replying
205 inline, replace this object with a static rendering from the
206 response.
@@ -391,7 +458,18 @@
391 });
392 });
393 });
394 }
395
 
 
 
 
 
 
 
 
 
 
 
396 })/*F.onPageLoad callback*/;
397 })(window.fossil);
398
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -25,34 +25,42 @@
25 #tabs;
26 /* Elements to disable while an XHR is pending. */
27 #toDisable = [];
28 /* DOM element of the current active tab. */
29 #activeTab;
30 /* Extra input[type=hidden] fields imported from fossil's
31 static page generation. */
32 #extraFields;
33
34 /**
35 */
36 constructor(opt){
37 opt = this.#opt = F.nu({
38 // todo: defaults once we determine the options
39 // replyTo: hash
40 // edit: hash
41 }, opt);
42 opt.isNewThread = !opt.replyTo && !opt.edit;
43 const e = this.#e = F.nu({
44 mimetype: F.nu(),
45 button: F.nu()
46 });
47 const wrapper = e.widget = D.addClass(D.div(), 'ForumPostEditor');
48 D.clearElement(wrapper);
49 if( !opt.inReplyTo ){
50 e.titleBar = D.addClass(D.div(),'titlebar');
51 e.title = D.addClass(D.input('text'), 'title');
52 e.title.setAttribute('maxlength', 125);
53 e.titleBar.append(
54 D.append(D.span(), "Title:"),
55 e.title
56 );
57 wrapper.append(e.titleBar);
58 }
59 e.mimetype.wrapper = D.addClass(D.div(), 'mimetype-wrapper');
60 e.mimetype.select = D.addClass(D.select(), 'mimetype-select');
61 this.#toDisable.push(e.mimetype.select);
62 e.mimetype.label = D.span();
63 e.mimetype.label.append(
64 D.a(F.repoUrl('markup_help'), 'Markup style'),
65 ':'
66 );
@@ -67,14 +75,18 @@
75 if( !i++ ) o.setAttribute('selected', '');
76 }
77
78 e.button.preview = D.button("Preview", e=>this.#preview());
79 e.button.submit = D.button("Submit");
80 if( 1 ){
81 F.confirmer(e.button.submit, {
82 confirmText: "Confirm submit...",
83 onconfirm: ()=>this.#submit()
84 });
85 }else{
86 e.button.submit.addEventListener('click', ()=>this.#submit());
87 }
88 e.button.submit.setAttribute('disabled', '');
89 e.buttons = D.addClass(D.div(), 'buttons');
90 wrapper.append(e.buttons);
91
92 e.err = D.addClass(D.div(), 'error', 'hidden');
@@ -86,32 +98,33 @@
98 e.tabs = D.attr(
99 D.addClass(D.div(), 'tab-container'),
100 'id', idPrefix+'-tabs'
101 );
102 this.#tabs = new F.TabManager(e.tabs);
103 this.#tabs.addEventListener('before-switch-to', (ev)=>{
104 this.#activeTab = ev.detail;
105 if( e.preview === this.#activeTab ){
106 this.#e.button.preview.click();
107 }
108 });
109 wrapper.append( e.tabs );
110
111 e.tabEdit = D.div();
112 e.tabEdit.classList.add('editor-wrapper');
113 e.editor = D.attr(
114 D.addClass(D.textarea(), 'editor'),
115 'placeholder',
116 'Your message to other forum-goers...'
117 );
118 e.tabEdit.append(e.editor);
119 e.tabEdit.dataset.tabLabel = 'Edit';
120 this.#tabs.addTab( e.tabEdit );
121 this.#tabs.switchToTab( e.tabEdit );
122
123 e.preview = D.addClass(D.div(), 'preview');
124 e.preview.dataset.tabLabel = 'Preview';
125 this.#tabs.addTab( e.preview );
 
 
 
 
 
 
126 }
127
128 if( F.user.enableDebug ){
129 e.debug = D.addClass(D.div(), 'debug');
130 e.debug.dataset.tabLabel = 'Debug';
@@ -147,11 +160,10 @@
160
161 get widget(){
162 return this.#e.widget;
163 }
164
 
165 /**
166 Reports an error by appending each argument to the error widget
167 and unhiding it. If passed no arugments, it clears and hides
168 the error widget.
169 */
@@ -164,14 +176,48 @@
176 }else{
177 e.classList.add('hidden');
178 }
179 }
180
181 /**
182 Adds a list of input[type=hidden] form fields to this object,
183 imported from the server-generated HTML. This is used for collecting,
184 e.g., the CSRF token.
185 */
186 addHiddenFields(list){
187 this.#extraFields ??= [];
188 for( const f of list ){
189 if( 'title'===f.name && this.#opt.isNewThread ){
190 this.#e.title.value = f.value;
191 }else{
192 this.#extraFields.push(f);
193 }
194 }
195 }
196
197 async #fetchPreview(content){
198 /* TODO: fetch preview */
199 const e = this.#e;
200 const fd = new FormData;
201 for(const f of this.#extraFields){
202 fd.append(f.name, f.value);
203 }
204 fd.append('mimetype', e.mimetype.select.value);
205 fd.append('content', content);
206 return window
207 .fetch(F.repoUrl('wikiajax/preview'), {
208 method: 'POST',
209 body: fd
210 })
211 .then(r=>r.text())
212 .then(t=>{
213 if( /^\{.*}$/.test(t) ){
214 const o = JSON.parse(t);
215 throw new Error(o.error);
216 }
217 return t;
218 });
219 }
220
221 async #preview(){
222 if( this.#isWaiting ) return;
223 const e = this.#e;
@@ -179,28 +225,49 @@
225 this.#tabs.switchToTab(e.preview);
226 /* Will recurse into here */
227 return;
228 }
229 this.#isWaiting = true;
230 D.clearElement(e.preview);
231 const content = e.editor.value.trim();
232 if( !content ){
233 return;
234 }
235 D.disable(this.#toDisable, e.button.submit);
236 e.preview.textContent = "Fetching preview...";
237 this.#fetchPreview(content)
238 .then((c)=>{
239 e.preview.innerHTML = c;
240 D.enable(this.#toDisable, e.button.submit);
241 })
242 .catch(err=>{
243 e.preview.textContent = "Error fetching preview: "+err.message;
244 this.reportError(err.message);
245 })
246 .finally(()=>{
247 this.#isWaiting = false;
248 D.enable(this.#toDisable);
249 console.warn("finally()!");
250 });
251 }
252
253 #validate(){
254 let v = this.#e.title.value.trim();
255 if( !v ){
256 this.reportError("A non-empty title is required.");
257 return;
258 }
259 return true;
260 }
261
262 #submit(){
263 if( this.#isWaiting ) return;
264 if( !this.#validate() ) return;
265 this.#isWaiting = true;
266 const e = this.#e;
267 D.disable(e.button.submit);
268 this.reportError("Submit is TODO.");
269 /*
270 TODO: save it, set #isWaiting=false, then handle error or
271 redirect to the post (if this is a new post) or, if replying
272 inline, replace this object with a static rendering from the
273 response.
@@ -391,7 +458,18 @@
458 });
459 });
460 });
461 }
462
463 const eForumNew = document.body.classList.contains('cpage-forumnew')
464 ? document.querySelector('#forumnew-placeholder')
465 : null;
466 if( eForumNew ){
467 /* /forumnew */
468 const fpe = new fossil.ForumPostEditor({});
469 fpe.addHiddenFields( eForumNew.querySelectorAll('input[type=hidden]') );
470 eForumNew.parentElement.insertBefore(fpe.widget, eForumNew);
471 eForumNew.remove();
472 fossil.page.fpe = fpe /* for testing via the console */;
473 }/*eForumNew*/
474 })/*F.onPageLoad callback*/;
475 })(window.fossil);
476
--- src/fossil.page.wikiedit.js
+++ src/fossil.page.wikiedit.js
@@ -1388,11 +1388,10 @@
13881388
setting.
13891389
*/
13901390
P.baseHrefRestore = function(){
13911391
this.base.tag.href = this.base.originalHref;
13921392
};
1393
-
13941393
13951394
/**
13961395
loadPage() loads the given wiki page and updates the relevant
13971396
UI elements to reflect the loaded state. If passed no arguments
13981397
then it re-uses the values from the currently-loaded page, reloading
@@ -1454,11 +1453,11 @@
14541453
onload(r);
14551454
}
14561455
});
14571456
return this;
14581457
};
1459
-
1458
+
14601459
/**
14611460
Fetches the page preview based on the contents and settings of
14621461
this page's input fields, and updates the UI with the
14631462
preview.
14641463
14651464
--- src/fossil.page.wikiedit.js
+++ src/fossil.page.wikiedit.js
@@ -1388,11 +1388,10 @@
1388 setting.
1389 */
1390 P.baseHrefRestore = function(){
1391 this.base.tag.href = this.base.originalHref;
1392 };
1393
1394
1395 /**
1396 loadPage() loads the given wiki page and updates the relevant
1397 UI elements to reflect the loaded state. If passed no arguments
1398 then it re-uses the values from the currently-loaded page, reloading
@@ -1454,11 +1453,11 @@
1454 onload(r);
1455 }
1456 });
1457 return this;
1458 };
1459
1460 /**
1461 Fetches the page preview based on the contents and settings of
1462 this page's input fields, and updates the UI with the
1463 preview.
1464
1465
--- src/fossil.page.wikiedit.js
+++ src/fossil.page.wikiedit.js
@@ -1388,11 +1388,10 @@
1388 setting.
1389 */
1390 P.baseHrefRestore = function(){
1391 this.base.tag.href = this.base.originalHref;
1392 };
 
1393
1394 /**
1395 loadPage() loads the given wiki page and updates the relevant
1396 UI elements to reflect the loaded state. If passed no arguments
1397 then it re-uses the values from the currently-loaded page, reloading
@@ -1454,11 +1453,11 @@
1453 onload(r);
1454 }
1455 });
1456 return this;
1457 };
1458
1459 /**
1460 Fetches the page preview based on the contents and settings of
1461 this page's input fields, and updates the UI with the
1462 preview.
1463
1464
--- src/style.forum.css
+++ src/style.forum.css
@@ -25,11 +25,12 @@
2525
.ForumPostEditor > .tab-container > .tabs > .tab-panel.debug label,
2626
.ForumPostEditor > .tab-container > .tabs > .tab-panel.debug input[type=checkbox]{
2727
cursor: pointer;
2828
}
2929
30
-.ForumPostEditor .tab-panel.editor-wrapper {
30
+
31
+.ForumPostEditor > .tab-container > .tabs > .tab-panel {
3132
display: flex;
3233
flex-direction: column;
3334
}
3435
3536
.ForumPostEditor .tab-panel.editor-wrapper > .editor {
@@ -40,17 +41,25 @@
4041
4142
.ForumPostEditor > .buttons > .mimetype-wrapper,
4243
.ForumPostEditor > .titlebar{
4344
display: flex;
4445
flex-direction: row;
46
+ font-size: 120%;
47
+ align-items: stretch;
4548
gap: 0.75em;
4649
}
47
-
50
+.ForumPostEditor > .titlebar{
51
+ align-items: center;
52
+}
4853
.ForumPostEditor > .titlebar > .title {
4954
flex-grow: 1;
55
+ /* Clear some globals... */
56
+ font-size: inherit;
57
+ max-width: initial;
58
+ color: initial;
5059
}
5160
5261
.ForumPostEditor > .buttons {
5362
display: flex;
5463
flex-direction: row;
5564
gap: 0.75em;
5665
}
5766
--- src/style.forum.css
+++ src/style.forum.css
@@ -25,11 +25,12 @@
25 .ForumPostEditor > .tab-container > .tabs > .tab-panel.debug label,
26 .ForumPostEditor > .tab-container > .tabs > .tab-panel.debug input[type=checkbox]{
27 cursor: pointer;
28 }
29
30 .ForumPostEditor .tab-panel.editor-wrapper {
 
31 display: flex;
32 flex-direction: column;
33 }
34
35 .ForumPostEditor .tab-panel.editor-wrapper > .editor {
@@ -40,17 +41,25 @@
40
41 .ForumPostEditor > .buttons > .mimetype-wrapper,
42 .ForumPostEditor > .titlebar{
43 display: flex;
44 flex-direction: row;
 
 
45 gap: 0.75em;
46 }
47
 
 
48 .ForumPostEditor > .titlebar > .title {
49 flex-grow: 1;
 
 
 
 
50 }
51
52 .ForumPostEditor > .buttons {
53 display: flex;
54 flex-direction: row;
55 gap: 0.75em;
56 }
57
--- src/style.forum.css
+++ src/style.forum.css
@@ -25,11 +25,12 @@
25 .ForumPostEditor > .tab-container > .tabs > .tab-panel.debug label,
26 .ForumPostEditor > .tab-container > .tabs > .tab-panel.debug input[type=checkbox]{
27 cursor: pointer;
28 }
29
30
31 .ForumPostEditor > .tab-container > .tabs > .tab-panel {
32 display: flex;
33 flex-direction: column;
34 }
35
36 .ForumPostEditor .tab-panel.editor-wrapper > .editor {
@@ -40,17 +41,25 @@
41
42 .ForumPostEditor > .buttons > .mimetype-wrapper,
43 .ForumPostEditor > .titlebar{
44 display: flex;
45 flex-direction: row;
46 font-size: 120%;
47 align-items: stretch;
48 gap: 0.75em;
49 }
50 .ForumPostEditor > .titlebar{
51 align-items: center;
52 }
53 .ForumPostEditor > .titlebar > .title {
54 flex-grow: 1;
55 /* Clear some globals... */
56 font-size: inherit;
57 max-width: initial;
58 color: initial;
59 }
60
61 .ForumPostEditor > .buttons {
62 display: flex;
63 flex-direction: row;
64 gap: 0.75em;
65 }
66
+2
--- src/wiki.c
+++ src/wiki.c
@@ -1109,10 +1109,12 @@
11091109
**
11101110
** URL params:
11111111
**
11121112
** mimetype = the wiki page mimetype (determines rendering style)
11131113
** content = the wiki page content
1114
+**
1115
+** Responds with a partial HTML document.
11141116
*/
11151117
static void wiki_ajax_route_preview(void){
11161118
const char * zContent = P("content");
11171119
11181120
if( zContent==0 ){
11191121
--- src/wiki.c
+++ src/wiki.c
@@ -1109,10 +1109,12 @@
1109 **
1110 ** URL params:
1111 **
1112 ** mimetype = the wiki page mimetype (determines rendering style)
1113 ** content = the wiki page content
 
 
1114 */
1115 static void wiki_ajax_route_preview(void){
1116 const char * zContent = P("content");
1117
1118 if( zContent==0 ){
1119
--- src/wiki.c
+++ src/wiki.c
@@ -1109,10 +1109,12 @@
1109 **
1110 ** URL params:
1111 **
1112 ** mimetype = the wiki page mimetype (determines rendering style)
1113 ** content = the wiki page content
1114 **
1115 ** Responds with a partial HTML document.
1116 */
1117 static void wiki_ajax_route_preview(void){
1118 const char * zContent = P("content");
1119
1120 if( zContent==0 ){
1121

Keyboard Shortcuts

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