Fossil SCM

Initial code for an ajax-friendly forum post editor. It does not yet speak to the backend but its UI is in place. Teach the /style.css?name=X mechanism to look for a feature-set name match if no page-name match is found so that all of the forum pages can get the new styles (the current mechanism cannot support both per-page and per-feature CSS in a single request).

stephan 2026-06-06 11:56 UTC attach-v2
Commit 64169ebd8888518182897c304674804d59f3a917e21ee1245bdf9fb0dc7721a2
--- src/builtin.c
+++ src/builtin.c
@@ -682,10 +682,11 @@
682682
CX("}\n"/*fossil.config.skin*/);
683683
CX("};\n"/* fossil.config */);
684684
CX("window.fossil.user = {");
685685
CX("name: %!j,", (g.zLogin&&*g.zLogin) ? g.zLogin : "guest");
686686
CX("isAdmin: %s,", (g.perm.Admin || g.perm.Setup) ? "true" : "false");
687
+ CX("mayAttachForum: %s,", g.perm.AttachForum ? "true" : "false");
687688
CX("enableDebug: %s", (g.perm.Debug || g.perm.Admin) ? "true" : "false");
688689
CX("};\n"/*fossil.user*/);
689690
CX("if(fossil.config.skin.isDark) "
690691
"document.body.classList.add('fossil-dark-style');\n");
691692
/*
692693
--- src/builtin.c
+++ src/builtin.c
@@ -682,10 +682,11 @@
682 CX("}\n"/*fossil.config.skin*/);
683 CX("};\n"/* fossil.config */);
684 CX("window.fossil.user = {");
685 CX("name: %!j,", (g.zLogin&&*g.zLogin) ? g.zLogin : "guest");
686 CX("isAdmin: %s,", (g.perm.Admin || g.perm.Setup) ? "true" : "false");
 
687 CX("enableDebug: %s", (g.perm.Debug || g.perm.Admin) ? "true" : "false");
688 CX("};\n"/*fossil.user*/);
689 CX("if(fossil.config.skin.isDark) "
690 "document.body.classList.add('fossil-dark-style');\n");
691 /*
692
--- src/builtin.c
+++ src/builtin.c
@@ -682,10 +682,11 @@
682 CX("}\n"/*fossil.config.skin*/);
683 CX("};\n"/* fossil.config */);
684 CX("window.fossil.user = {");
685 CX("name: %!j,", (g.zLogin&&*g.zLogin) ? g.zLogin : "guest");
686 CX("isAdmin: %s,", (g.perm.Admin || g.perm.Setup) ? "true" : "false");
687 CX("mayAttachForum: %s,", g.perm.AttachForum ? "true" : "false");
688 CX("enableDebug: %s", (g.perm.Debug || g.perm.Admin) ? "true" : "false");
689 CX("};\n"/*fossil.user*/);
690 CX("if(fossil.config.skin.isDark) "
691 "document.body.classList.add('fossil-dark-style');\n");
692 /*
693
+1 -1
--- src/forum.c
+++ src/forum.c
@@ -1483,11 +1483,11 @@
14831483
** to all forum-related pages. It does not include page-specific
14841484
** code (e.g. "forum.js").
14851485
*/
14861486
static void forum_emit_js(void){
14871487
builtin_fossil_js_bundle_or("copybutton", "pikchr", "confirmer",
1488
- NULL);
1488
+ "attach", "tabs", NULL);
14891489
builtin_request_js("fossil.page.forumpost.js");
14901490
}
14911491
14921492
/*
14931493
** WEBPAGE: forumpost
14941494
--- src/forum.c
+++ src/forum.c
@@ -1483,11 +1483,11 @@
1483 ** to all forum-related pages. It does not include page-specific
1484 ** code (e.g. "forum.js").
1485 */
1486 static void forum_emit_js(void){
1487 builtin_fossil_js_bundle_or("copybutton", "pikchr", "confirmer",
1488 NULL);
1489 builtin_request_js("fossil.page.forumpost.js");
1490 }
1491
1492 /*
1493 ** WEBPAGE: forumpost
1494
--- src/forum.c
+++ src/forum.c
@@ -1483,11 +1483,11 @@
1483 ** to all forum-related pages. It does not include page-specific
1484 ** code (e.g. "forum.js").
1485 */
1486 static void forum_emit_js(void){
1487 builtin_fossil_js_bundle_or("copybutton", "pikchr", "confirmer",
1488 "attach", "tabs", NULL);
1489 builtin_request_js("fossil.page.forumpost.js");
1490 }
1491
1492 /*
1493 ** WEBPAGE: forumpost
1494
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -88,10 +88,11 @@
8888
D.button(this.#opt.addButtonLabel || 'Add attachment',
8989
()=>this.#addRow()),
9090
'attach-add-button'
9191
);
9292
eBtnAdd.type = 'button';
93
+ opt.ownsAddButton = true;
9394
this.#e.err = D.addClass(D.div(), 'error', 'hidden');
9495
this.#e.body.append(this.#e.err);
9596
this.#e.err.addEventListener('dblclick',()=>this.reportError());
9697
9798
const eControls = this.#e.controls =
@@ -219,14 +220,28 @@
219220
D.disable(b);
220221
//F.toast.warning("Attachment form limit reached.");
221222
}else{
222223
b.classList.remove('hidden');
223224
D.enable(b);
224
- this.#e.body.append(this.#e.controls/*move to the end*/);
225
+ if( this.#opt.ownsAddButton ){
226
+ this.#e.body.append(this.#e.controls/*move to the end*/);
227
+ }
225228
}
226229
}
227230
231
+ /**
232
+ Returns the "Add" button widget, Passing control of it to the
233
+ caller so that they can place it in another location. This
234
+ object will still manage its enabled/disabled/hidden state but
235
+ will no longer move it when adding a row.
236
+ */
237
+ takeAddButton(){
238
+ if( this.#opt.ownsAddButton ){
239
+ this.#opt.ownsAddButton;
240
+ }
241
+ return this.#e.btnAdd;
242
+ }
228243
/**
229244
Sets rowObj.e.err up with an error message, or clears it if
230245
passed only 1 argument.
231246
*/
232247
#rowError(rowObj,...msg){
233248
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -88,10 +88,11 @@
88 D.button(this.#opt.addButtonLabel || 'Add attachment',
89 ()=>this.#addRow()),
90 'attach-add-button'
91 );
92 eBtnAdd.type = 'button';
 
93 this.#e.err = D.addClass(D.div(), 'error', 'hidden');
94 this.#e.body.append(this.#e.err);
95 this.#e.err.addEventListener('dblclick',()=>this.reportError());
96
97 const eControls = this.#e.controls =
@@ -219,14 +220,28 @@
219 D.disable(b);
220 //F.toast.warning("Attachment form limit reached.");
221 }else{
222 b.classList.remove('hidden');
223 D.enable(b);
224 this.#e.body.append(this.#e.controls/*move to the end*/);
 
 
225 }
226 }
227
 
 
 
 
 
 
 
 
 
 
 
 
228 /**
229 Sets rowObj.e.err up with an error message, or clears it if
230 passed only 1 argument.
231 */
232 #rowError(rowObj,...msg){
233
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -88,10 +88,11 @@
88 D.button(this.#opt.addButtonLabel || 'Add attachment',
89 ()=>this.#addRow()),
90 'attach-add-button'
91 );
92 eBtnAdd.type = 'button';
93 opt.ownsAddButton = true;
94 this.#e.err = D.addClass(D.div(), 'error', 'hidden');
95 this.#e.body.append(this.#e.err);
96 this.#e.err.addEventListener('dblclick',()=>this.reportError());
97
98 const eControls = this.#e.controls =
@@ -219,14 +220,28 @@
220 D.disable(b);
221 //F.toast.warning("Attachment form limit reached.");
222 }else{
223 b.classList.remove('hidden');
224 D.enable(b);
225 if( this.#opt.ownsAddButton ){
226 this.#e.body.append(this.#e.controls/*move to the end*/);
227 }
228 }
229 }
230
231 /**
232 Returns the "Add" button widget, Passing control of it to the
233 caller so that they can place it in another location. This
234 object will still manage its enabled/disabled/hidden state but
235 will no longer move it when adding a row.
236 */
237 takeAddButton(){
238 if( this.#opt.ownsAddButton ){
239 this.#opt.ownsAddButton;
240 }
241 return this.#e.btnAdd;
242 }
243 /**
244 Sets rowObj.e.err up with an error message, or clears it if
245 passed only 1 argument.
246 */
247 #rowError(rowObj,...msg){
248
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -1,11 +1,218 @@
1
+/**
2
+ Code for the forum family of pages. Requires fossil.X where X is
3
+ (copybutton, pikchr, confirmer, attach, tabs).
4
+*/
15
(function(F/*the fossil object*/){
26
"use strict";
37
/* JS code for /forumpost and friends. Requires fossil.dom
48
and can optionally use fossil.pikchr. */
59
const P = F.page, D = F.dom;
610
11
+ let idCounter = 0;
12
+ /**
13
+ A WIP forum post editor widget for both new posts and responses.
14
+ */
15
+ class ForumPostEditor {
16
+ /* Options */
17
+ #opt;
18
+ /* Dom elements */
19
+ #e;
20
+ /* Attacher */
21
+ #att;
22
+ /* Is waiting on a pending remote response. */
23
+ #isWaiting = false;
24
+ /* F.TabManager */
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
+ opt.isNew = !opt.edit && !opt.replyTo;
38
+ const e = this.#e = F.nu({
39
+ mimetype: F.nu(),
40
+ button: F.nu()
41
+ });
42
+ const wrapper = e.widget = D.addClass(D.div(), 'ForumPostEditor');
43
+ D.clearElement(wrapper);
44
+ if( !opt.inReplyTo ){
45
+ e.titleBar = D.addClass(D.div(),'titlebar');
46
+ e.title = D.addClass(D.input('text'), 'title');
47
+ e.titleBar.append(
48
+ D.append(D.span(), "Title:"),
49
+ e.title
50
+ );
51
+ wrapper.append(e.titleBar);
52
+ }
53
+ e.mimetype.wrapper = D.addClass(D.div(), 'mimetype-wrapper');
54
+ e.mimetype.select = D.addClass(D.select(), 'mimetype-select');
55
+ e.mimetype.label = D.span();
56
+ e.mimetype.label.append(
57
+ D.a(F.repoUrl('markup_help'), 'Markup style'),
58
+ ':'
59
+ );
60
+ e.mimetype.wrapper.append(e.mimetype.label, e.mimetype.select);
61
+ let i = 0;
62
+ for(const [k,v] of Object.entries({
63
+ 'text/x-markdown': 'Markdown',
64
+ 'text/x-fossil-wiki': 'Fossil Wiki',
65
+ 'text/plain': 'Plain text'
66
+ })) {
67
+ const o = D.option(e.mimetype.select, k, v);
68
+ if( !i++ ) o.setAttribute('selected', '');
69
+ }
70
+
71
+ e.button.preview = D.button("Preview", e=>this.#preview());
72
+ e.button.submit = D.button("Submit", e=>this.#submit());
73
+ e.button.submit.setAttribute('disabled', '');
74
+ e.buttons = D.addClass(D.div(), 'buttons');
75
+ wrapper.append(e.buttons);
76
+
77
+ e.err = D.addClass(D.div(), 'error', 'hidden');
78
+ wrapper.append(e.err);
79
+ e.err.addEventListener('dblclick',()=>this.reportError());
80
+
81
+ const idPrefix = 'FormPostEditor'+(++idCounter)/* TabManager requires IDs */;
82
+ { /* Tabs... */
83
+ e.tabs = D.attr(
84
+ D.addClass(D.div(), 'tab-container'),
85
+ 'id', idPrefix+'-tabs'
86
+ );
87
+ this.#tabs = new F.TabManager(e.tabs);
88
+ wrapper.append( e.tabs );
89
+
90
+ e.tabEdit = D.div();
91
+ e.tabEdit.classList.add('editor-wrapper');
92
+ e.editor = D.attr(
93
+ D.addClass(D.textarea(), 'editor'),
94
+ 'placeholder',
95
+ 'Your content...'
96
+ );
97
+ e.tabEdit.append(e.editor);
98
+ e.tabEdit.dataset.tabLabel = 'Edit';
99
+ this.#tabs.addTab( e.tabEdit );
100
+
101
+ e.preview = D.addClass(D.div(), 'preview');
102
+ e.preview.dataset.tabLabel = 'Preview';
103
+ this.#tabs.addTab( e.preview );
104
+ this.#tabs.addEventListener('before-switch-to', (ev)=>{
105
+ this.#activeTab = ev.detail;
106
+ if( e.preview === this.#activeTab ){
107
+ this.#e.button.preview.click();
108
+ }
109
+ });
110
+ }
111
+
112
+ if( F.user.enableDebug ){
113
+ e.debug = D.addClass(D.div(), 'debug');
114
+ e.debug.dataset.tabLabel = 'Debug';
115
+ e.debug.setAttribute('id', idPrefix+'-debug');
116
+ for(const [k,v] of Object.entries({
117
+ dryrun: 'Dry run',
118
+ domod: 'Require moderation approval',
119
+ showqp: 'Show query parameters',
120
+ fpsilent: 'Do not send notification emails'
121
+ })){
122
+ const lbl = D.label(false, v);
123
+ lbl.prepend(D.checkbox(k));
124
+ e.debug.append(lbl);
125
+ }
126
+ this.#tabs.addTab(e.debug);
127
+ }
128
+ e.buttons.append(e.mimetype.wrapper);
129
+ if( F.user.mayAttachForum ){
130
+ this.#att = new F.Attacher({
131
+ addButtonLabel: 'Attach'
132
+ });
133
+ e.buttons.append( e.button.addAttach = this.#att.takeAddButton() );
134
+ this.#toDisable.push( e.button.addAttach );
135
+ }
136
+ e.buttons.append(e.button.preview, e.button.submit);
137
+ this.#toDisable.push(e.button.preview);
138
+ if( this.#att ){
139
+ wrapper.append(this.#att.widget);
140
+ }
141
+ }/*constructor*/
142
+
143
+ get widget(){
144
+ return this.#e.widget;
145
+ }
146
+
147
+
148
+ /**
149
+ Reports an error by appending each argument to the error widget
150
+ and unhiding it. If passed no arugments, it clears and hides
151
+ the error widget.
152
+ */
153
+ reportError(...msg){
154
+ const e = this.#e.err;
155
+ D.clearElement(e);
156
+ if( msg.length ){
157
+ e.classList.remove('hidden');
158
+ e.append(...msg);
159
+ }else{
160
+ e.classList.add('hidden');
161
+ }
162
+ }
163
+
164
+ async #fetchPreview(){
165
+ /* TODO: fetch preview */
166
+ this.#isWaiting = false;
167
+ D.enable(this.#toDisable);
168
+ }
169
+
170
+ async #preview(){
171
+ if( this.#isWaiting ) return;
172
+ const e = this.#e;
173
+ if( e.preview !== this.#activeTab ){
174
+ this.#tabs.switchToTab(e.preview);
175
+ /* Will recurse into here */
176
+ return;
177
+ }
178
+ this.#isWaiting = true;
179
+ D.disable(this.#toDisable, e.button.submit);
180
+ e.preview.textContent = "Fetching preview...";
181
+ this.#fetchPreview()
182
+ .then(()=>{
183
+ e.preview.textContent = "TODO: actually fetch the preview "+Date.now();
184
+ D.enable(this.#toDisable, e.button.submit);
185
+ })
186
+ .catch(e=>{
187
+ this.reportError(e.message);
188
+ D.enable(this.#toDisable);
189
+ });
190
+ }
191
+
192
+ #submit(){
193
+ if( this.#isWaiting ) return;
194
+ this.#isWaiting = true;
195
+ const e = this.#e;
196
+ D.disable(e.button.submit);
197
+ /*
198
+ TODO: save it, set #isWaiting=false, then handle error or
199
+ redirect to the post (if this is a new post) or, if replying
200
+ inline, replace this object with a static rendering from the
201
+ response.
202
+ */
203
+ }
204
+
205
+ async #fetchPost(){
206
+ /*
207
+ TODO: when editing an existing post, fetch the raw body of the
208
+ post and populate this.e.
209
+ */
210
+ }
211
+ }/*ForumPostEditor*/;
212
+ F.ForumPostEditor = ForumPostEditor;
213
+
7214
/**
8215
When the page is loaded, this handler does the following:
9216
10217
- Installs expand/collapse UI elements on "long" posts and collapses
11218
them.
12219
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -1,11 +1,218 @@
 
 
 
 
1 (function(F/*the fossil object*/){
2 "use strict";
3 /* JS code for /forumpost and friends. Requires fossil.dom
4 and can optionally use fossil.pikchr. */
5 const P = F.page, D = F.dom;
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7 /**
8 When the page is loaded, this handler does the following:
9
10 - Installs expand/collapse UI elements on "long" posts and collapses
11 them.
12
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -1,11 +1,218 @@
1 /**
2 Code for the forum family of pages. Requires fossil.X where X is
3 (copybutton, pikchr, confirmer, attach, tabs).
4 */
5 (function(F/*the fossil object*/){
6 "use strict";
7 /* JS code for /forumpost and friends. Requires fossil.dom
8 and can optionally use fossil.pikchr. */
9 const P = F.page, D = F.dom;
10
11 let idCounter = 0;
12 /**
13 A WIP forum post editor widget for both new posts and responses.
14 */
15 class ForumPostEditor {
16 /* Options */
17 #opt;
18 /* Dom elements */
19 #e;
20 /* Attacher */
21 #att;
22 /* Is waiting on a pending remote response. */
23 #isWaiting = false;
24 /* F.TabManager */
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 opt.isNew = !opt.edit && !opt.replyTo;
38 const e = this.#e = F.nu({
39 mimetype: F.nu(),
40 button: F.nu()
41 });
42 const wrapper = e.widget = D.addClass(D.div(), 'ForumPostEditor');
43 D.clearElement(wrapper);
44 if( !opt.inReplyTo ){
45 e.titleBar = D.addClass(D.div(),'titlebar');
46 e.title = D.addClass(D.input('text'), 'title');
47 e.titleBar.append(
48 D.append(D.span(), "Title:"),
49 e.title
50 );
51 wrapper.append(e.titleBar);
52 }
53 e.mimetype.wrapper = D.addClass(D.div(), 'mimetype-wrapper');
54 e.mimetype.select = D.addClass(D.select(), 'mimetype-select');
55 e.mimetype.label = D.span();
56 e.mimetype.label.append(
57 D.a(F.repoUrl('markup_help'), 'Markup style'),
58 ':'
59 );
60 e.mimetype.wrapper.append(e.mimetype.label, e.mimetype.select);
61 let i = 0;
62 for(const [k,v] of Object.entries({
63 'text/x-markdown': 'Markdown',
64 'text/x-fossil-wiki': 'Fossil Wiki',
65 'text/plain': 'Plain text'
66 })) {
67 const o = D.option(e.mimetype.select, k, v);
68 if( !i++ ) o.setAttribute('selected', '');
69 }
70
71 e.button.preview = D.button("Preview", e=>this.#preview());
72 e.button.submit = D.button("Submit", e=>this.#submit());
73 e.button.submit.setAttribute('disabled', '');
74 e.buttons = D.addClass(D.div(), 'buttons');
75 wrapper.append(e.buttons);
76
77 e.err = D.addClass(D.div(), 'error', 'hidden');
78 wrapper.append(e.err);
79 e.err.addEventListener('dblclick',()=>this.reportError());
80
81 const idPrefix = 'FormPostEditor'+(++idCounter)/* TabManager requires IDs */;
82 { /* Tabs... */
83 e.tabs = D.attr(
84 D.addClass(D.div(), 'tab-container'),
85 'id', idPrefix+'-tabs'
86 );
87 this.#tabs = new F.TabManager(e.tabs);
88 wrapper.append( e.tabs );
89
90 e.tabEdit = D.div();
91 e.tabEdit.classList.add('editor-wrapper');
92 e.editor = D.attr(
93 D.addClass(D.textarea(), 'editor'),
94 'placeholder',
95 'Your content...'
96 );
97 e.tabEdit.append(e.editor);
98 e.tabEdit.dataset.tabLabel = 'Edit';
99 this.#tabs.addTab( e.tabEdit );
100
101 e.preview = D.addClass(D.div(), 'preview');
102 e.preview.dataset.tabLabel = 'Preview';
103 this.#tabs.addTab( e.preview );
104 this.#tabs.addEventListener('before-switch-to', (ev)=>{
105 this.#activeTab = ev.detail;
106 if( e.preview === this.#activeTab ){
107 this.#e.button.preview.click();
108 }
109 });
110 }
111
112 if( F.user.enableDebug ){
113 e.debug = D.addClass(D.div(), 'debug');
114 e.debug.dataset.tabLabel = 'Debug';
115 e.debug.setAttribute('id', idPrefix+'-debug');
116 for(const [k,v] of Object.entries({
117 dryrun: 'Dry run',
118 domod: 'Require moderation approval',
119 showqp: 'Show query parameters',
120 fpsilent: 'Do not send notification emails'
121 })){
122 const lbl = D.label(false, v);
123 lbl.prepend(D.checkbox(k));
124 e.debug.append(lbl);
125 }
126 this.#tabs.addTab(e.debug);
127 }
128 e.buttons.append(e.mimetype.wrapper);
129 if( F.user.mayAttachForum ){
130 this.#att = new F.Attacher({
131 addButtonLabel: 'Attach'
132 });
133 e.buttons.append( e.button.addAttach = this.#att.takeAddButton() );
134 this.#toDisable.push( e.button.addAttach );
135 }
136 e.buttons.append(e.button.preview, e.button.submit);
137 this.#toDisable.push(e.button.preview);
138 if( this.#att ){
139 wrapper.append(this.#att.widget);
140 }
141 }/*constructor*/
142
143 get widget(){
144 return this.#e.widget;
145 }
146
147
148 /**
149 Reports an error by appending each argument to the error widget
150 and unhiding it. If passed no arugments, it clears and hides
151 the error widget.
152 */
153 reportError(...msg){
154 const e = this.#e.err;
155 D.clearElement(e);
156 if( msg.length ){
157 e.classList.remove('hidden');
158 e.append(...msg);
159 }else{
160 e.classList.add('hidden');
161 }
162 }
163
164 async #fetchPreview(){
165 /* TODO: fetch preview */
166 this.#isWaiting = false;
167 D.enable(this.#toDisable);
168 }
169
170 async #preview(){
171 if( this.#isWaiting ) return;
172 const e = this.#e;
173 if( e.preview !== this.#activeTab ){
174 this.#tabs.switchToTab(e.preview);
175 /* Will recurse into here */
176 return;
177 }
178 this.#isWaiting = true;
179 D.disable(this.#toDisable, e.button.submit);
180 e.preview.textContent = "Fetching preview...";
181 this.#fetchPreview()
182 .then(()=>{
183 e.preview.textContent = "TODO: actually fetch the preview "+Date.now();
184 D.enable(this.#toDisable, e.button.submit);
185 })
186 .catch(e=>{
187 this.reportError(e.message);
188 D.enable(this.#toDisable);
189 });
190 }
191
192 #submit(){
193 if( this.#isWaiting ) return;
194 this.#isWaiting = true;
195 const e = this.#e;
196 D.disable(e.button.submit);
197 /*
198 TODO: save it, set #isWaiting=false, then handle error or
199 redirect to the post (if this is a new post) or, if replying
200 inline, replace this object with a static rendering from the
201 response.
202 */
203 }
204
205 async #fetchPost(){
206 /*
207 TODO: when editing an existing post, fetch the raw body of the
208 post and populate this.e.
209 */
210 }
211 }/*ForumPostEditor*/;
212 F.ForumPostEditor = ForumPostEditor;
213
214 /**
215 When the page is loaded, this handler does the following:
216
217 - Installs expand/collapse UI elements on "long" posts and collapses
218 them.
219
--- src/fossil.tabs.js
+++ src/fossil.tabs.js
@@ -7,11 +7,11 @@
77
/**
88
Creates a TabManager. If passed a truthy first argument, it is
99
passed to init(). If passed a truthy second argument, it must be
1010
an Object holding configuration options:
1111
12
- {
12
+ {
1313
tabAccessKeys: boolean (=true)
1414
If true, tab buttons are assigned "accesskey" values
1515
equal to their 1-based tab number.
1616
}
1717
*/
@@ -56,11 +56,11 @@
5656
/**
5757
Initializes the tabs associated with the given tab container
5858
(DOM element or selector for a single element). This must be
5959
called once before using any other member functions of a given
6060
instance, noting that the constructor will call this if it is
61
- passed an argument.
61
+ passed an argument.
6262
6363
The tab container must have an 'id' attribute. This function
6464
looks through the DOM for all elements which have
6565
data-tab-parent=thatId. For each one it creates a button to
6666
switch to that tab and moves the element into this.e.tabs,
@@ -150,10 +150,11 @@
150150
e.target.$manager.switchToTab(e.target.$tab);
151151
};
152152
}
153153
tab = tabArg(tab);
154154
tab.remove();
155
+ tab.classList.add('hidden');
155156
D.append(this.e.tabs, D.addClass(tab,'tab-panel'));
156157
const tabCount = this.e.tabBar.childNodes.length+1;
157158
const lbl = tab.dataset.tabLabel || 'Tab #'+tabCount;
158159
const btn = D.addClass(D.append(D.span(), lbl), 'tab-button');
159160
D.append(this.e.tabBar,btn);
160161
--- src/fossil.tabs.js
+++ src/fossil.tabs.js
@@ -7,11 +7,11 @@
7 /**
8 Creates a TabManager. If passed a truthy first argument, it is
9 passed to init(). If passed a truthy second argument, it must be
10 an Object holding configuration options:
11
12 {
13 tabAccessKeys: boolean (=true)
14 If true, tab buttons are assigned "accesskey" values
15 equal to their 1-based tab number.
16 }
17 */
@@ -56,11 +56,11 @@
56 /**
57 Initializes the tabs associated with the given tab container
58 (DOM element or selector for a single element). This must be
59 called once before using any other member functions of a given
60 instance, noting that the constructor will call this if it is
61 passed an argument.
62
63 The tab container must have an 'id' attribute. This function
64 looks through the DOM for all elements which have
65 data-tab-parent=thatId. For each one it creates a button to
66 switch to that tab and moves the element into this.e.tabs,
@@ -150,10 +150,11 @@
150 e.target.$manager.switchToTab(e.target.$tab);
151 };
152 }
153 tab = tabArg(tab);
154 tab.remove();
 
155 D.append(this.e.tabs, D.addClass(tab,'tab-panel'));
156 const tabCount = this.e.tabBar.childNodes.length+1;
157 const lbl = tab.dataset.tabLabel || 'Tab #'+tabCount;
158 const btn = D.addClass(D.append(D.span(), lbl), 'tab-button');
159 D.append(this.e.tabBar,btn);
160
--- src/fossil.tabs.js
+++ src/fossil.tabs.js
@@ -7,11 +7,11 @@
7 /**
8 Creates a TabManager. If passed a truthy first argument, it is
9 passed to init(). If passed a truthy second argument, it must be
10 an Object holding configuration options:
11
12 {
13 tabAccessKeys: boolean (=true)
14 If true, tab buttons are assigned "accesskey" values
15 equal to their 1-based tab number.
16 }
17 */
@@ -56,11 +56,11 @@
56 /**
57 Initializes the tabs associated with the given tab container
58 (DOM element or selector for a single element). This must be
59 called once before using any other member functions of a given
60 instance, noting that the constructor will call this if it is
61 passed an argument.
62
63 The tab container must have an 'id' attribute. This function
64 looks through the DOM for all elements which have
65 data-tab-parent=thatId. For each one it creates a button to
66 switch to that tab and moves the element into this.e.tabs,
@@ -150,10 +150,11 @@
150 e.target.$manager.switchToTab(e.target.$tab);
151 };
152 }
153 tab = tabArg(tab);
154 tab.remove();
155 tab.classList.add('hidden');
156 D.append(this.e.tabs, D.addClass(tab,'tab-panel'));
157 const tabCount = this.e.tabBar.childNodes.length+1;
158 const lbl = tab.dataset.tabLabel || 'Tab #'+tabCount;
159 const btn = D.addClass(D.append(D.span(), lbl), 'tab-button');
160 D.append(this.e.tabBar,btn);
161
--- src/main.mk
+++ src/main.mk
@@ -276,10 +276,11 @@
276276
$(SRCDIR)/sounds/e.wav \
277277
$(SRCDIR)/sounds/f.wav \
278278
$(SRCDIR)/style.admin_log.css \
279279
$(SRCDIR)/style.chat.css \
280280
$(SRCDIR)/style.fileedit.css \
281
+ $(SRCDIR)/style.forum.css \
281282
$(SRCDIR)/style.pikchrshow.css \
282283
$(SRCDIR)/style.uvlist.css \
283284
$(SRCDIR)/style.wikiedit.css \
284285
$(SRCDIR)/tree.js \
285286
$(SRCDIR)/useredit.js \
286287
--- src/main.mk
+++ src/main.mk
@@ -276,10 +276,11 @@
276 $(SRCDIR)/sounds/e.wav \
277 $(SRCDIR)/sounds/f.wav \
278 $(SRCDIR)/style.admin_log.css \
279 $(SRCDIR)/style.chat.css \
280 $(SRCDIR)/style.fileedit.css \
 
281 $(SRCDIR)/style.pikchrshow.css \
282 $(SRCDIR)/style.uvlist.css \
283 $(SRCDIR)/style.wikiedit.css \
284 $(SRCDIR)/tree.js \
285 $(SRCDIR)/useredit.js \
286
--- src/main.mk
+++ src/main.mk
@@ -276,10 +276,11 @@
276 $(SRCDIR)/sounds/e.wav \
277 $(SRCDIR)/sounds/f.wav \
278 $(SRCDIR)/style.admin_log.css \
279 $(SRCDIR)/style.chat.css \
280 $(SRCDIR)/style.fileedit.css \
281 $(SRCDIR)/style.forum.css \
282 $(SRCDIR)/style.pikchrshow.css \
283 $(SRCDIR)/style.uvlist.css \
284 $(SRCDIR)/style.wikiedit.css \
285 $(SRCDIR)/tree.js \
286 $(SRCDIR)/useredit.js \
287
+18 -2
--- src/style.c
+++ src/style.c
@@ -384,10 +384,11 @@
384384
385385
/* Use this for the $current_page variable if it is not NULL. If it
386386
** is NULL then use g.zPath.
387387
*/
388388
static char *local_zCurrentPage = 0;
389
+static char *local_zCurrentFeature = 0;
389390
390391
/*
391392
** Set the desired $current_page to something other than g.zPath
392393
*/
393394
void style_set_current_page(const char *zFormat, ...){
@@ -419,12 +420,13 @@
419420
420421
/* Initialize the URL to its baseline */
421422
url = empty_blob;
422423
blob_appendf(&url, "%R/style.css");
423424
424
- /* If page-specific CSS exists for the current page, then append
425
- ** the pathname for the page-specific CSS. The default CSS is
425
+ /* If page- or feature-specific CSS exists for the current page,
426
+ ** then append the pathname for the page-specific CSS. The default
427
+ ** CSS is
426428
**
427429
** /style.css
428430
**
429431
** But for the "/wikiedit" page (to name but one example), we
430432
** append a path as follows:
@@ -432,14 +434,25 @@
432434
** /style.css/wikiedit
433435
**
434436
** The /style.css page (implemented below) will detect this extra "wikiedit"
435437
** path information and include the page-specific CSS along with the
436438
** default CSS when it delivers the page.
439
+ **
440
+ ** Prior to 2026-06-06, this only looked at zPage but /forum and
441
+ ** friends need a per-feature style, so it now falls back to
442
+ ** local_zCurrentFeature. The current mechanism cannot support both
443
+ ** concurrently in a single request.
437444
*/
438445
zBuiltin = mprintf("style.%s.css", zPage);
439446
if( builtin_file(zBuiltin,0)!=0 ){
440447
blob_appendf(&url, "/%t", zPage);
448
+ }else if( local_zCurrentFeature ){
449
+ fossil_free(zBuiltin);
450
+ zBuiltin = mprintf("style.%s.css", local_zCurrentFeature);
451
+ if( builtin_file(zBuiltin,0)!=0 ){
452
+ blob_appendf(&url, "/%t", local_zCurrentFeature);
453
+ }
441454
}
442455
fossil_free(zBuiltin);
443456
444457
/* Add query parameters that will change whenever the skin changes
445458
** or after any updates to the CSS files
@@ -727,10 +740,12 @@
727740
** style_init_th1_vars() because that uses Th_MaybeStore() instead to
728741
** allow webpage implementations to call this before style_header()
729742
** to override that "maybe" default with something better.
730743
*/
731744
void style_set_current_feature(const char* zFeature){
745
+ fossil_free( local_zCurrentFeature );
746
+ local_zCurrentFeature = fossil_strdup(zFeature);
732747
Th_Store("current_feature", zFeature);
733748
}
734749
735750
/*
736751
** Returns the current mainmenu value from either the --mainmenu flag
@@ -1258,10 +1273,11 @@
12581273
"** Page-specific CSS for \"%s\"\n"
12591274
"***********************************************************/\n",
12601275
zPage);
12611276
blob_append(pOut, zBuiltin, nFile);
12621277
fossil_free(zFile);
1278
+ zFile = 0;
12631279
return;
12641280
}
12651281
/* Potential TODO: check for aliases/page groups. e.g. group all
12661282
** /forumXYZ CSS into one file, all /setupXYZ into another, etc. As
12671283
** of this writing, doing so would only shave a few kb from
12681284
12691285
ADDED src/style.forum.css
--- src/style.c
+++ src/style.c
@@ -384,10 +384,11 @@
384
385 /* Use this for the $current_page variable if it is not NULL. If it
386 ** is NULL then use g.zPath.
387 */
388 static char *local_zCurrentPage = 0;
 
389
390 /*
391 ** Set the desired $current_page to something other than g.zPath
392 */
393 void style_set_current_page(const char *zFormat, ...){
@@ -419,12 +420,13 @@
419
420 /* Initialize the URL to its baseline */
421 url = empty_blob;
422 blob_appendf(&url, "%R/style.css");
423
424 /* If page-specific CSS exists for the current page, then append
425 ** the pathname for the page-specific CSS. The default CSS is
 
426 **
427 ** /style.css
428 **
429 ** But for the "/wikiedit" page (to name but one example), we
430 ** append a path as follows:
@@ -432,14 +434,25 @@
432 ** /style.css/wikiedit
433 **
434 ** The /style.css page (implemented below) will detect this extra "wikiedit"
435 ** path information and include the page-specific CSS along with the
436 ** default CSS when it delivers the page.
 
 
 
 
 
437 */
438 zBuiltin = mprintf("style.%s.css", zPage);
439 if( builtin_file(zBuiltin,0)!=0 ){
440 blob_appendf(&url, "/%t", zPage);
 
 
 
 
 
 
441 }
442 fossil_free(zBuiltin);
443
444 /* Add query parameters that will change whenever the skin changes
445 ** or after any updates to the CSS files
@@ -727,10 +740,12 @@
727 ** style_init_th1_vars() because that uses Th_MaybeStore() instead to
728 ** allow webpage implementations to call this before style_header()
729 ** to override that "maybe" default with something better.
730 */
731 void style_set_current_feature(const char* zFeature){
 
 
732 Th_Store("current_feature", zFeature);
733 }
734
735 /*
736 ** Returns the current mainmenu value from either the --mainmenu flag
@@ -1258,10 +1273,11 @@
1258 "** Page-specific CSS for \"%s\"\n"
1259 "***********************************************************/\n",
1260 zPage);
1261 blob_append(pOut, zBuiltin, nFile);
1262 fossil_free(zFile);
 
1263 return;
1264 }
1265 /* Potential TODO: check for aliases/page groups. e.g. group all
1266 ** /forumXYZ CSS into one file, all /setupXYZ into another, etc. As
1267 ** of this writing, doing so would only shave a few kb from
1268
1269 DDED src/style.forum.css
--- src/style.c
+++ src/style.c
@@ -384,10 +384,11 @@
384
385 /* Use this for the $current_page variable if it is not NULL. If it
386 ** is NULL then use g.zPath.
387 */
388 static char *local_zCurrentPage = 0;
389 static char *local_zCurrentFeature = 0;
390
391 /*
392 ** Set the desired $current_page to something other than g.zPath
393 */
394 void style_set_current_page(const char *zFormat, ...){
@@ -419,12 +420,13 @@
420
421 /* Initialize the URL to its baseline */
422 url = empty_blob;
423 blob_appendf(&url, "%R/style.css");
424
425 /* If page- or feature-specific CSS exists for the current page,
426 ** then append the pathname for the page-specific CSS. The default
427 ** CSS is
428 **
429 ** /style.css
430 **
431 ** But for the "/wikiedit" page (to name but one example), we
432 ** append a path as follows:
@@ -432,14 +434,25 @@
434 ** /style.css/wikiedit
435 **
436 ** The /style.css page (implemented below) will detect this extra "wikiedit"
437 ** path information and include the page-specific CSS along with the
438 ** default CSS when it delivers the page.
439 **
440 ** Prior to 2026-06-06, this only looked at zPage but /forum and
441 ** friends need a per-feature style, so it now falls back to
442 ** local_zCurrentFeature. The current mechanism cannot support both
443 ** concurrently in a single request.
444 */
445 zBuiltin = mprintf("style.%s.css", zPage);
446 if( builtin_file(zBuiltin,0)!=0 ){
447 blob_appendf(&url, "/%t", zPage);
448 }else if( local_zCurrentFeature ){
449 fossil_free(zBuiltin);
450 zBuiltin = mprintf("style.%s.css", local_zCurrentFeature);
451 if( builtin_file(zBuiltin,0)!=0 ){
452 blob_appendf(&url, "/%t", local_zCurrentFeature);
453 }
454 }
455 fossil_free(zBuiltin);
456
457 /* Add query parameters that will change whenever the skin changes
458 ** or after any updates to the CSS files
@@ -727,10 +740,12 @@
740 ** style_init_th1_vars() because that uses Th_MaybeStore() instead to
741 ** allow webpage implementations to call this before style_header()
742 ** to override that "maybe" default with something better.
743 */
744 void style_set_current_feature(const char* zFeature){
745 fossil_free( local_zCurrentFeature );
746 local_zCurrentFeature = fossil_strdup(zFeature);
747 Th_Store("current_feature", zFeature);
748 }
749
750 /*
751 ** Returns the current mainmenu value from either the --mainmenu flag
@@ -1258,10 +1273,11 @@
1273 "** Page-specific CSS for \"%s\"\n"
1274 "***********************************************************/\n",
1275 zPage);
1276 blob_append(pOut, zBuiltin, nFile);
1277 fossil_free(zFile);
1278 zFile = 0;
1279 return;
1280 }
1281 /* Potential TODO: check for aliases/page groups. e.g. group all
1282 ** /forumXYZ CSS into one file, all /setupXYZ into another, etc. As
1283 ** of this writing, doing so would only shave a few kb from
1284
1285 DDED src/style.forum.css
--- a/src/style.forum.css
+++ b/src/style.forum.css
@@ -0,0 +1,56 @@
1
+/* Styles specific to the forum family of pages */
2
+
3
+.ForumPostEditor {
4
+ display: flex;
5
+ flex-direction: column;
6
+ gap: 1em;
7
+}
8
+
9
+.ForumPostEditor > .tab-bar{
10
+}
11
+
12
+.ForumPostEditor > .tab-container {
13
+}
14
+
15
+.ForumPostEditor > .tab-container > .tabs {
16
+ min-height: 16em;
17
+}
18
+
19
+.ForumPostEditor > .tab-container > .tabs > .tab-panel.debug {
20
+ display: flex;
21
+ flex-direction: column;
22
+ gap: 0.75em;
23
+}
24
+
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 {
36
+ max-width: initial;
37
+ min-height: 18em;
38
+ flex-grow: 1
39
+}
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
+}
--- a/src/style.forum.css
+++ b/src/style.forum.css
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/src/style.forum.css
+++ b/src/style.forum.css
@@ -0,0 +1,56 @@
1 /* Styles specific to the forum family of pages */
2
3 .ForumPostEditor {
4 display: flex;
5 flex-direction: column;
6 gap: 1em;
7 }
8
9 .ForumPostEditor > .tab-bar{
10 }
11
12 .ForumPostEditor > .tab-container {
13 }
14
15 .ForumPostEditor > .tab-container > .tabs {
16 min-height: 16em;
17 }
18
19 .ForumPostEditor > .tab-container > .tabs > .tab-panel.debug {
20 display: flex;
21 flex-direction: column;
22 gap: 0.75em;
23 }
24
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 {
36 max-width: initial;
37 min-height: 18em;
38 flex-grow: 1
39 }
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 }
--- win/Makefile.mingw
+++ win/Makefile.mingw
@@ -662,10 +662,11 @@
662662
$(SRCDIR)/sounds/e.wav \
663663
$(SRCDIR)/sounds/f.wav \
664664
$(SRCDIR)/style.admin_log.css \
665665
$(SRCDIR)/style.chat.css \
666666
$(SRCDIR)/style.fileedit.css \
667
+ $(SRCDIR)/style.forum.css \
667668
$(SRCDIR)/style.pikchrshow.css \
668669
$(SRCDIR)/style.uvlist.css \
669670
$(SRCDIR)/style.wikiedit.css \
670671
$(SRCDIR)/tree.js \
671672
$(SRCDIR)/useredit.js \
672673
--- win/Makefile.mingw
+++ win/Makefile.mingw
@@ -662,10 +662,11 @@
662 $(SRCDIR)/sounds/e.wav \
663 $(SRCDIR)/sounds/f.wav \
664 $(SRCDIR)/style.admin_log.css \
665 $(SRCDIR)/style.chat.css \
666 $(SRCDIR)/style.fileedit.css \
 
667 $(SRCDIR)/style.pikchrshow.css \
668 $(SRCDIR)/style.uvlist.css \
669 $(SRCDIR)/style.wikiedit.css \
670 $(SRCDIR)/tree.js \
671 $(SRCDIR)/useredit.js \
672
--- win/Makefile.mingw
+++ win/Makefile.mingw
@@ -662,10 +662,11 @@
662 $(SRCDIR)/sounds/e.wav \
663 $(SRCDIR)/sounds/f.wav \
664 $(SRCDIR)/style.admin_log.css \
665 $(SRCDIR)/style.chat.css \
666 $(SRCDIR)/style.fileedit.css \
667 $(SRCDIR)/style.forum.css \
668 $(SRCDIR)/style.pikchrshow.css \
669 $(SRCDIR)/style.uvlist.css \
670 $(SRCDIR)/style.wikiedit.css \
671 $(SRCDIR)/tree.js \
672 $(SRCDIR)/useredit.js \
673
--- win/Makefile.msc
+++ win/Makefile.msc
@@ -627,10 +627,11 @@
627627
"$(SRCDIR)\sounds\e.wav" \
628628
"$(SRCDIR)\sounds\f.wav" \
629629
"$(SRCDIR)\style.admin_log.css" \
630630
"$(SRCDIR)\style.chat.css" \
631631
"$(SRCDIR)\style.fileedit.css" \
632
+ "$(SRCDIR)\style.forum.css" \
632633
"$(SRCDIR)\style.pikchrshow.css" \
633634
"$(SRCDIR)\style.uvlist.css" \
634635
"$(SRCDIR)\style.wikiedit.css" \
635636
"$(SRCDIR)\tree.js" \
636637
"$(SRCDIR)\useredit.js" \
@@ -1266,10 +1267,11 @@
12661267
echo "$(SRCDIR)\sounds/e.wav" >> $@
12671268
echo "$(SRCDIR)\sounds/f.wav" >> $@
12681269
echo "$(SRCDIR)\style.admin_log.css" >> $@
12691270
echo "$(SRCDIR)\style.chat.css" >> $@
12701271
echo "$(SRCDIR)\style.fileedit.css" >> $@
1272
+ echo "$(SRCDIR)\style.forum.css" >> $@
12711273
echo "$(SRCDIR)\style.pikchrshow.css" >> $@
12721274
echo "$(SRCDIR)\style.uvlist.css" >> $@
12731275
echo "$(SRCDIR)\style.wikiedit.css" >> $@
12741276
echo "$(SRCDIR)\tree.js" >> $@
12751277
echo "$(SRCDIR)\useredit.js" >> $@
12761278
--- win/Makefile.msc
+++ win/Makefile.msc
@@ -627,10 +627,11 @@
627 "$(SRCDIR)\sounds\e.wav" \
628 "$(SRCDIR)\sounds\f.wav" \
629 "$(SRCDIR)\style.admin_log.css" \
630 "$(SRCDIR)\style.chat.css" \
631 "$(SRCDIR)\style.fileedit.css" \
 
632 "$(SRCDIR)\style.pikchrshow.css" \
633 "$(SRCDIR)\style.uvlist.css" \
634 "$(SRCDIR)\style.wikiedit.css" \
635 "$(SRCDIR)\tree.js" \
636 "$(SRCDIR)\useredit.js" \
@@ -1266,10 +1267,11 @@
1266 echo "$(SRCDIR)\sounds/e.wav" >> $@
1267 echo "$(SRCDIR)\sounds/f.wav" >> $@
1268 echo "$(SRCDIR)\style.admin_log.css" >> $@
1269 echo "$(SRCDIR)\style.chat.css" >> $@
1270 echo "$(SRCDIR)\style.fileedit.css" >> $@
 
1271 echo "$(SRCDIR)\style.pikchrshow.css" >> $@
1272 echo "$(SRCDIR)\style.uvlist.css" >> $@
1273 echo "$(SRCDIR)\style.wikiedit.css" >> $@
1274 echo "$(SRCDIR)\tree.js" >> $@
1275 echo "$(SRCDIR)\useredit.js" >> $@
1276
--- win/Makefile.msc
+++ win/Makefile.msc
@@ -627,10 +627,11 @@
627 "$(SRCDIR)\sounds\e.wav" \
628 "$(SRCDIR)\sounds\f.wav" \
629 "$(SRCDIR)\style.admin_log.css" \
630 "$(SRCDIR)\style.chat.css" \
631 "$(SRCDIR)\style.fileedit.css" \
632 "$(SRCDIR)\style.forum.css" \
633 "$(SRCDIR)\style.pikchrshow.css" \
634 "$(SRCDIR)\style.uvlist.css" \
635 "$(SRCDIR)\style.wikiedit.css" \
636 "$(SRCDIR)\tree.js" \
637 "$(SRCDIR)\useredit.js" \
@@ -1266,10 +1267,11 @@
1267 echo "$(SRCDIR)\sounds/e.wav" >> $@
1268 echo "$(SRCDIR)\sounds/f.wav" >> $@
1269 echo "$(SRCDIR)\style.admin_log.css" >> $@
1270 echo "$(SRCDIR)\style.chat.css" >> $@
1271 echo "$(SRCDIR)\style.fileedit.css" >> $@
1272 echo "$(SRCDIR)\style.forum.css" >> $@
1273 echo "$(SRCDIR)\style.pikchrshow.css" >> $@
1274 echo "$(SRCDIR)\style.uvlist.css" >> $@
1275 echo "$(SRCDIR)\style.wikiedit.css" >> $@
1276 echo "$(SRCDIR)\tree.js" >> $@
1277 echo "$(SRCDIR)\useredit.js" >> $@
1278

Keyboard Shortcuts

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