Fossil SCM

Docs. Add a help tab and move the markup format reference link into there to save toolbar space.

stephan 2026-06-06 18:10 UTC forum-editor-2026
Commit 205d80ed04941838750657e4d5de76f3bd9e39b3726ff25c2615d93c473bc7c9
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -417,15 +417,28 @@
417417
for(let r of this.#rows){
418418
if( r.file?.name===name ) return r;
419419
}
420420
}
421421
422
+ /**
423
+ Injects the given File object as the attached content for the
424
+ given row. If the object's name collides with another row,
425
+ rowObj is removed from this widget and the old row is instead
426
+ re-populated with the new file.
427
+
428
+ If rowObj.overrideName is set then the given file gets wrapped
429
+ with that name before attaching it, and that property is
430
+ removed from rowObj. This is intended only for communicating
431
+ auto-generated names for pasted data.
432
+ */
422433
#injestBlob(rowObj, file){
423434
if( !file ) return;
424435
const old = this.#rowMatchingName(file.name);
425436
if( rowObj.overrideName ){
426
- file = new File([file], rowObj.overrideName, {type: file.type});
437
+ if( rowObj.overrideName !== file.name ){
438
+ file = new File([file], rowObj.overrideName, {type: file.type});
439
+ }
427440
rowObj.overrideName = undefined;
428441
}
429442
if( old && rowObj !== old ){
430443
/*
431444
Fossil attachments treat the name as a unique-per-target
432445
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -417,15 +417,28 @@
417 for(let r of this.#rows){
418 if( r.file?.name===name ) return r;
419 }
420 }
421
 
 
 
 
 
 
 
 
 
 
 
422 #injestBlob(rowObj, file){
423 if( !file ) return;
424 const old = this.#rowMatchingName(file.name);
425 if( rowObj.overrideName ){
426 file = new File([file], rowObj.overrideName, {type: file.type});
 
 
427 rowObj.overrideName = undefined;
428 }
429 if( old && rowObj !== old ){
430 /*
431 Fossil attachments treat the name as a unique-per-target
432
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -417,15 +417,28 @@
417 for(let r of this.#rows){
418 if( r.file?.name===name ) return r;
419 }
420 }
421
422 /**
423 Injects the given File object as the attached content for the
424 given row. If the object's name collides with another row,
425 rowObj is removed from this widget and the old row is instead
426 re-populated with the new file.
427
428 If rowObj.overrideName is set then the given file gets wrapped
429 with that name before attaching it, and that property is
430 removed from rowObj. This is intended only for communicating
431 auto-generated names for pasted data.
432 */
433 #injestBlob(rowObj, file){
434 if( !file ) return;
435 const old = this.#rowMatchingName(file.name);
436 if( rowObj.overrideName ){
437 if( rowObj.overrideName !== file.name ){
438 file = new File([file], rowObj.overrideName, {type: file.type});
439 }
440 rowObj.overrideName = undefined;
441 }
442 if( old && rowObj !== old ){
443 /*
444 Fossil attachments treat the name as a unique-per-target
445
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -53,10 +53,11 @@
5353
button: F.nu()
5454
});
5555
const wrapper = e.widget = D.addClass(D.div(), 'ForumPostEditor');
5656
D.clearElement(wrapper);
5757
if( !opt.inReplyTo ){
58
+ /* Title... */
5859
e.titleBar = D.addClass(D.div(),'titlebar');
5960
e.title = D.addClass(D.input('text'), 'title');
6061
e.title.setAttribute('maxlength', 125);
6162
e.titleBar.append(
6263
D.append(D.span(), "Title:"),
@@ -65,65 +66,82 @@
6566
if( opt.draftKey ){
6667
const key = opt.draftKey+'.title';
6768
e.title.addEventListener('blur', ()=>{
6869
F.storage.set(key, e.title.value)
6970
});
70
- e.title.value = F.storage.get(key,'');
71
+ e.title.value = opt.title || F.storage.get(key, '');
72
+ }else if( opt.title ){
73
+ e.title.value = opt.title;
7174
}
7275
wrapper.append(e.titleBar);
7376
}
7477
75
- { /* Mimetype bits... */
78
+ { /* Mimetype... */
7679
e.mimetype.wrapper = D.addClass(D.div(), 'mimetype-wrapper');
7780
e.mimetype.select = D.addClass(D.select(), 'mimetype-select');
7881
this.#toDisable.push(e.mimetype.select);
7982
let i = 0;
83
+ D.option(e.mimetype.select, '', 'Markdown format').disabled = true;
8084
for(const [k,v] of Object.entries({
8185
'text/x-markdown': 'Markdown',
8286
'text/x-fossil-wiki': 'Fossil Wiki',
8387
'text/plain': 'Plain text'
8488
})) {
8589
const o = D.option(e.mimetype.select, k, v);
86
- if( !i++ ) o.setAttribute('selected', '');
87
- }
88
- e.mimetype.label = D.span();
89
- e.mimetype.label.append(
90
- D.a(F.repoUrl('markup_help'), 'Markup style'),
91
- ':'
92
- );
93
- e.mimetype.wrapper.append(e.mimetype.label, e.mimetype.select);
94
- }
95
-
96
- e.button.preview = D.button("Preview", e=>this.#preview());
97
- e.button.submit = D.button("Submit");
98
- if( 1 ){
99
- F.confirmer(e.button.submit, {
100
- confirmText: "Confirm submit...",
101
- onconfirm: ()=>this.#submit()
102
- });
103
- }else{
104
- e.button.submit.addEventListener('click', ()=>this.#submit());
105
- }
106
- e.button.submit.setAttribute('disabled', '');
107
- e.buttons = D.addClass(D.div(), 'buttons');
108
- wrapper.append(e.buttons);
109
-
110
- e.err = D.addClass(D.div(), 'error', 'hidden');
111
- wrapper.append(e.err);
112
- e.err.addEventListener('dblclick',()=>this.reportError());
90
+ if( (opt.isNewThread && !i++)
91
+ || opt.mimetype===k ) o.setAttribute('selected', '');
92
+ }
93
+ if( 0 ){
94
+ e.mimetype.label = D.span();
95
+ e.mimetype.label.append(
96
+ D.a(F.repoUrl('markup_help'), 'Markup style'),
97
+ ':'
98
+ );
99
+ e.mimetype.wrapper.append(e.mimetype.label);
100
+ }
101
+ e.mimetype.wrapper.append(e.mimetype.select);
102
+ }
103
+
104
+ { /* Preview/submit buttons... */
105
+ e.button.preview = D.button("Preview", e=>this.#preview());
106
+ e.button.submit = D.button("Submit");
107
+ if( 1 ){
108
+ F.confirmer(e.button.submit, {
109
+ confirmText: "Confirm submit...",
110
+ onconfirm: ()=>this.#submit()
111
+ });
112
+ }else{
113
+ e.button.submit.addEventListener('click', ()=>this.#submit());
114
+ }
115
+ e.button.submit.setAttribute('disabled', '');
116
+ e.buttons = D.addClass(D.div(), 'buttons');
117
+ wrapper.append(e.buttons);
118
+
119
+ e.error = D.addClass(D.div(), 'error', 'hidden');
120
+ wrapper.append(e.error);
121
+ e.error.addEventListener('dblclick',()=>this.reportError());
122
+ }
113123
114124
const idPrefix = 'FormPostEditor'+(++idCounter)/* TabManager requires IDs */;
115125
{ /* Main tabs... */
116126
e.tabs = D.attr(
117127
D.addClass(D.div(), 'tab-container'),
118128
'id', idPrefix+'-tabs'
119129
);
120130
this.#tabs = new F.TabManager(e.tabs);
121131
this.#tabs.addEventListener('before-switch-to', (ev)=>{
122
- this.#activeTab = ev.detail;
123
- if( e.preview === this.#activeTab ){
124
- this.#e.button.preview.click();
132
+ console.debug("Switching to tab",ev.detail);
133
+ switch( (this.#activeTab = ev.detail) ){
134
+ case e.preview:
135
+ this.#e.button.preview.click();
136
+ break;
137
+ case e.help:
138
+ if( e.help.$needsInit ){
139
+ delete e.help.$needsInit;
140
+ this.#initHelpTab();
141
+ }
142
+ break;
125143
}
126144
});
127145
wrapper.append( e.tabs );
128146
129147
e.tabEdit = D.div();
@@ -173,19 +191,10 @@
173191
the editor because people will open the editor, change the
174192
status, and tap submit, resulting in a whole new, unedited
175193
copy of the post, differing only in the new 'status' tag
176194
added to it.
177195
*/
178
- let i = 0;
179
- for(const [k,v] of Object.entries({
180
- 'text/x-markdown': 'Markdown',
181
- 'text/x-fossil-wiki': 'Fossil Wiki',
182
- 'text/plain': 'Plain text'
183
- })) {
184
- const o = D.option(e.mimetype.select, k, v);
185
- if( !i++ ) o.setAttribute('selected', '');
186
- }
187196
if( F.config.forumStatuses?.length>0 ){
188197
const sel = e.status = D.select();
189198
D.option(sel, "", "- Status -").disabled = true;
190199
for( const status of F.config.forumStatuses ){
191200
D.option(sel, status.value, status.label);
@@ -209,10 +218,15 @@
209218
/* Reminder: we don't currently have a way to disable/enable
210219
an Attacher's controls. */
211220
}
212221
e.buttons.append(e.button.preview, e.button.submit);
213222
this.#toDisable.push(e.button.preview);
223
+
224
+ e.help = D.attr(D.div(), 'id', idPrefix+'-help');
225
+ e.help.$needsInit = true;
226
+ e.help.dataset.tabLabel = 'Help';
227
+ this.#tabs.addTab(e.help);
214228
215229
if( opt.hiddenFields ){
216230
this.addHiddenFields( opt.hiddenFields );
217231
delete opt.hiddenFields;
218232
}
@@ -304,11 +318,11 @@
304318
Reports an error by appending each argument to the error widget
305319
and unhiding it. If passed no arugments, it clears and hides
306320
the error widget.
307321
*/
308322
reportError(...msg){
309
- const e = this.#e.err;
323
+ const e = this.#e.error;
310324
D.clearElement(e);
311325
if( msg.length ){
312326
e.classList.remove('hidden');
313327
e.append(...msg);
314328
console.error('ForumPostEditor:',...msg);
@@ -340,10 +354,21 @@
340354
}
341355
342356
get title(){
343357
return this.#e.title.value;
344358
}
359
+
360
+ #initHelpTab(){
361
+ const eh = this.#e.help;
362
+ const list = D.ul();
363
+ D.append(
364
+ D.li(list),
365
+ D.attr(D.a(F.repoUrl('markup_help'), 'Markup styles'),
366
+ 'target', '_new')
367
+ );
368
+ eh.append(list);
369
+ }
345370
346371
#newFormData(addThisContent){
347372
const fd = new FormData;
348373
for(const f of this.#extraFields){
349374
fd.append(f.name, f.value);
@@ -645,12 +670,13 @@
645670
if( eForumNew ){
646671
/* /forumnew */
647672
const fpe = new fossil.ForumPostEditor({
648673
draftKey: 'forumnew',
649674
hiddenFields: eForumNew.querySelectorAll('input[type=hidden]')
675
+ //mimetype: 'text/plain'
650676
});
651677
eForumNew.parentElement.insertBefore(fpe.widget, eForumNew);
652678
eForumNew.remove();
653679
fossil.page.fpe = fpe /* for testing via the console */;
654680
}/*eForumNew*/
655681
})/*F.onPageLoad callback*/;
656682
})(window.fossil);
657683
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -53,10 +53,11 @@
53 button: F.nu()
54 });
55 const wrapper = e.widget = D.addClass(D.div(), 'ForumPostEditor');
56 D.clearElement(wrapper);
57 if( !opt.inReplyTo ){
 
58 e.titleBar = D.addClass(D.div(),'titlebar');
59 e.title = D.addClass(D.input('text'), 'title');
60 e.title.setAttribute('maxlength', 125);
61 e.titleBar.append(
62 D.append(D.span(), "Title:"),
@@ -65,65 +66,82 @@
65 if( opt.draftKey ){
66 const key = opt.draftKey+'.title';
67 e.title.addEventListener('blur', ()=>{
68 F.storage.set(key, e.title.value)
69 });
70 e.title.value = F.storage.get(key,'');
 
 
71 }
72 wrapper.append(e.titleBar);
73 }
74
75 { /* Mimetype bits... */
76 e.mimetype.wrapper = D.addClass(D.div(), 'mimetype-wrapper');
77 e.mimetype.select = D.addClass(D.select(), 'mimetype-select');
78 this.#toDisable.push(e.mimetype.select);
79 let i = 0;
 
80 for(const [k,v] of Object.entries({
81 'text/x-markdown': 'Markdown',
82 'text/x-fossil-wiki': 'Fossil Wiki',
83 'text/plain': 'Plain text'
84 })) {
85 const o = D.option(e.mimetype.select, k, v);
86 if( !i++ ) o.setAttribute('selected', '');
87 }
88 e.mimetype.label = D.span();
89 e.mimetype.label.append(
90 D.a(F.repoUrl('markup_help'), 'Markup style'),
91 ':'
92 );
93 e.mimetype.wrapper.append(e.mimetype.label, e.mimetype.select);
94 }
95
96 e.button.preview = D.button("Preview", e=>this.#preview());
97 e.button.submit = D.button("Submit");
98 if( 1 ){
99 F.confirmer(e.button.submit, {
100 confirmText: "Confirm submit...",
101 onconfirm: ()=>this.#submit()
102 });
103 }else{
104 e.button.submit.addEventListener('click', ()=>this.#submit());
105 }
106 e.button.submit.setAttribute('disabled', '');
107 e.buttons = D.addClass(D.div(), 'buttons');
108 wrapper.append(e.buttons);
109
110 e.err = D.addClass(D.div(), 'error', 'hidden');
111 wrapper.append(e.err);
112 e.err.addEventListener('dblclick',()=>this.reportError());
 
 
 
 
 
 
113
114 const idPrefix = 'FormPostEditor'+(++idCounter)/* TabManager requires IDs */;
115 { /* Main tabs... */
116 e.tabs = D.attr(
117 D.addClass(D.div(), 'tab-container'),
118 'id', idPrefix+'-tabs'
119 );
120 this.#tabs = new F.TabManager(e.tabs);
121 this.#tabs.addEventListener('before-switch-to', (ev)=>{
122 this.#activeTab = ev.detail;
123 if( e.preview === this.#activeTab ){
124 this.#e.button.preview.click();
 
 
 
 
 
 
 
 
125 }
126 });
127 wrapper.append( e.tabs );
128
129 e.tabEdit = D.div();
@@ -173,19 +191,10 @@
173 the editor because people will open the editor, change the
174 status, and tap submit, resulting in a whole new, unedited
175 copy of the post, differing only in the new 'status' tag
176 added to it.
177 */
178 let i = 0;
179 for(const [k,v] of Object.entries({
180 'text/x-markdown': 'Markdown',
181 'text/x-fossil-wiki': 'Fossil Wiki',
182 'text/plain': 'Plain text'
183 })) {
184 const o = D.option(e.mimetype.select, k, v);
185 if( !i++ ) o.setAttribute('selected', '');
186 }
187 if( F.config.forumStatuses?.length>0 ){
188 const sel = e.status = D.select();
189 D.option(sel, "", "- Status -").disabled = true;
190 for( const status of F.config.forumStatuses ){
191 D.option(sel, status.value, status.label);
@@ -209,10 +218,15 @@
209 /* Reminder: we don't currently have a way to disable/enable
210 an Attacher's controls. */
211 }
212 e.buttons.append(e.button.preview, e.button.submit);
213 this.#toDisable.push(e.button.preview);
 
 
 
 
 
214
215 if( opt.hiddenFields ){
216 this.addHiddenFields( opt.hiddenFields );
217 delete opt.hiddenFields;
218 }
@@ -304,11 +318,11 @@
304 Reports an error by appending each argument to the error widget
305 and unhiding it. If passed no arugments, it clears and hides
306 the error widget.
307 */
308 reportError(...msg){
309 const e = this.#e.err;
310 D.clearElement(e);
311 if( msg.length ){
312 e.classList.remove('hidden');
313 e.append(...msg);
314 console.error('ForumPostEditor:',...msg);
@@ -340,10 +354,21 @@
340 }
341
342 get title(){
343 return this.#e.title.value;
344 }
 
 
 
 
 
 
 
 
 
 
 
345
346 #newFormData(addThisContent){
347 const fd = new FormData;
348 for(const f of this.#extraFields){
349 fd.append(f.name, f.value);
@@ -645,12 +670,13 @@
645 if( eForumNew ){
646 /* /forumnew */
647 const fpe = new fossil.ForumPostEditor({
648 draftKey: 'forumnew',
649 hiddenFields: eForumNew.querySelectorAll('input[type=hidden]')
 
650 });
651 eForumNew.parentElement.insertBefore(fpe.widget, eForumNew);
652 eForumNew.remove();
653 fossil.page.fpe = fpe /* for testing via the console */;
654 }/*eForumNew*/
655 })/*F.onPageLoad callback*/;
656 })(window.fossil);
657
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -53,10 +53,11 @@
53 button: F.nu()
54 });
55 const wrapper = e.widget = D.addClass(D.div(), 'ForumPostEditor');
56 D.clearElement(wrapper);
57 if( !opt.inReplyTo ){
58 /* Title... */
59 e.titleBar = D.addClass(D.div(),'titlebar');
60 e.title = D.addClass(D.input('text'), 'title');
61 e.title.setAttribute('maxlength', 125);
62 e.titleBar.append(
63 D.append(D.span(), "Title:"),
@@ -65,65 +66,82 @@
66 if( opt.draftKey ){
67 const key = opt.draftKey+'.title';
68 e.title.addEventListener('blur', ()=>{
69 F.storage.set(key, e.title.value)
70 });
71 e.title.value = opt.title || F.storage.get(key, '');
72 }else if( opt.title ){
73 e.title.value = opt.title;
74 }
75 wrapper.append(e.titleBar);
76 }
77
78 { /* Mimetype... */
79 e.mimetype.wrapper = D.addClass(D.div(), 'mimetype-wrapper');
80 e.mimetype.select = D.addClass(D.select(), 'mimetype-select');
81 this.#toDisable.push(e.mimetype.select);
82 let i = 0;
83 D.option(e.mimetype.select, '', 'Markdown format').disabled = true;
84 for(const [k,v] of Object.entries({
85 'text/x-markdown': 'Markdown',
86 'text/x-fossil-wiki': 'Fossil Wiki',
87 'text/plain': 'Plain text'
88 })) {
89 const o = D.option(e.mimetype.select, k, v);
90 if( (opt.isNewThread && !i++)
91 || opt.mimetype===k ) o.setAttribute('selected', '');
92 }
93 if( 0 ){
94 e.mimetype.label = D.span();
95 e.mimetype.label.append(
96 D.a(F.repoUrl('markup_help'), 'Markup style'),
97 ':'
98 );
99 e.mimetype.wrapper.append(e.mimetype.label);
100 }
101 e.mimetype.wrapper.append(e.mimetype.select);
102 }
103
104 { /* Preview/submit buttons... */
105 e.button.preview = D.button("Preview", e=>this.#preview());
106 e.button.submit = D.button("Submit");
107 if( 1 ){
108 F.confirmer(e.button.submit, {
109 confirmText: "Confirm submit...",
110 onconfirm: ()=>this.#submit()
111 });
112 }else{
113 e.button.submit.addEventListener('click', ()=>this.#submit());
114 }
115 e.button.submit.setAttribute('disabled', '');
116 e.buttons = D.addClass(D.div(), 'buttons');
117 wrapper.append(e.buttons);
118
119 e.error = D.addClass(D.div(), 'error', 'hidden');
120 wrapper.append(e.error);
121 e.error.addEventListener('dblclick',()=>this.reportError());
122 }
123
124 const idPrefix = 'FormPostEditor'+(++idCounter)/* TabManager requires IDs */;
125 { /* Main tabs... */
126 e.tabs = D.attr(
127 D.addClass(D.div(), 'tab-container'),
128 'id', idPrefix+'-tabs'
129 );
130 this.#tabs = new F.TabManager(e.tabs);
131 this.#tabs.addEventListener('before-switch-to', (ev)=>{
132 console.debug("Switching to tab",ev.detail);
133 switch( (this.#activeTab = ev.detail) ){
134 case e.preview:
135 this.#e.button.preview.click();
136 break;
137 case e.help:
138 if( e.help.$needsInit ){
139 delete e.help.$needsInit;
140 this.#initHelpTab();
141 }
142 break;
143 }
144 });
145 wrapper.append( e.tabs );
146
147 e.tabEdit = D.div();
@@ -173,19 +191,10 @@
191 the editor because people will open the editor, change the
192 status, and tap submit, resulting in a whole new, unedited
193 copy of the post, differing only in the new 'status' tag
194 added to it.
195 */
 
 
 
 
 
 
 
 
 
196 if( F.config.forumStatuses?.length>0 ){
197 const sel = e.status = D.select();
198 D.option(sel, "", "- Status -").disabled = true;
199 for( const status of F.config.forumStatuses ){
200 D.option(sel, status.value, status.label);
@@ -209,10 +218,15 @@
218 /* Reminder: we don't currently have a way to disable/enable
219 an Attacher's controls. */
220 }
221 e.buttons.append(e.button.preview, e.button.submit);
222 this.#toDisable.push(e.button.preview);
223
224 e.help = D.attr(D.div(), 'id', idPrefix+'-help');
225 e.help.$needsInit = true;
226 e.help.dataset.tabLabel = 'Help';
227 this.#tabs.addTab(e.help);
228
229 if( opt.hiddenFields ){
230 this.addHiddenFields( opt.hiddenFields );
231 delete opt.hiddenFields;
232 }
@@ -304,11 +318,11 @@
318 Reports an error by appending each argument to the error widget
319 and unhiding it. If passed no arugments, it clears and hides
320 the error widget.
321 */
322 reportError(...msg){
323 const e = this.#e.error;
324 D.clearElement(e);
325 if( msg.length ){
326 e.classList.remove('hidden');
327 e.append(...msg);
328 console.error('ForumPostEditor:',...msg);
@@ -340,10 +354,21 @@
354 }
355
356 get title(){
357 return this.#e.title.value;
358 }
359
360 #initHelpTab(){
361 const eh = this.#e.help;
362 const list = D.ul();
363 D.append(
364 D.li(list),
365 D.attr(D.a(F.repoUrl('markup_help'), 'Markup styles'),
366 'target', '_new')
367 );
368 eh.append(list);
369 }
370
371 #newFormData(addThisContent){
372 const fd = new FormData;
373 for(const f of this.#extraFields){
374 fd.append(f.name, f.value);
@@ -645,12 +670,13 @@
670 if( eForumNew ){
671 /* /forumnew */
672 const fpe = new fossil.ForumPostEditor({
673 draftKey: 'forumnew',
674 hiddenFields: eForumNew.querySelectorAll('input[type=hidden]')
675 //mimetype: 'text/plain'
676 });
677 eForumNew.parentElement.insertBefore(fpe.widget, eForumNew);
678 eForumNew.remove();
679 fossil.page.fpe = fpe /* for testing via the console */;
680 }/*eForumNew*/
681 })/*F.onPageLoad callback*/;
682 })(window.fossil);
683

Keyboard Shortcuts

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