Fossil SCM

Forum edit/response widget now (more or less) smoothly appears in place instead of jarring the whole viewport.

stephan 2026-06-09 07:36 UTC forum-editor-2026
Commit 78478f45e91a527f02af9c38b10c3b09fd7b7367f2661df0ae66a992c49c86fd
--- src/default.css
+++ src/default.css
@@ -2125,10 +2125,49 @@
21252125
.Attacher .attach-controls .attach-add-button {
21262126
padding: 0.5em 1em;
21272127
cursor: pointer;
21282128
flex-grow: 2;
21292129
}
2130
+
2131
+.animate-entrance {
2132
+ animation: slideFadeIn 0.25s ease-out forwards;
2133
+ transform-origin: top;
2134
+ overflow: hidden /*prevent content bleeding during expansion*/;
2135
+}
2136
+.animate-exit {
2137
+ animation: slideFadeOut 0.25s ease-in forwards;
2138
+ transform-origin: top;
2139
+ overflow: hidden;
2140
+}
2141
+@keyframes slideFadeIn {
2142
+ 0% {
2143
+ opacity: 0;
2144
+ transform: translateY(-1em/*must match slideFadeOut*/);
2145
+ max-height: 0;
2146
+ }
2147
+ 100% {
2148
+ opacity: 1;
2149
+ transform: translateY(0);
2150
+ max-height: 100em /*a value safely larger than the widget*/;
2151
+ }
2152
+}
2153
+@keyframes slideFadeOut {
2154
+ 0% {
2155
+ opacity: 1;
2156
+ transform: translateY(0);
2157
+ max-height: 100em /*must match slideFadeIn*/;
2158
+ }
2159
+ 100% {
2160
+ opacity: 0;
2161
+ transform: translateY(-1em/*must match slideFadeIn*/);
2162
+ max-height: 0;
2163
+ padding-top: 0;
2164
+ padding-bottom: 0;
2165
+ margin-top: 0;
2166
+ margin-bottom: 0;
2167
+ }
2168
+}
21302169
21312170
/* Objects in the "desktoponly" class are invisible on mobile */
21322171
@media screen and (max-width: 600px) {
21332172
.desktoponly {
21342173
display: none;
21352174
--- src/default.css
+++ src/default.css
@@ -2125,10 +2125,49 @@
2125 .Attacher .attach-controls .attach-add-button {
2126 padding: 0.5em 1em;
2127 cursor: pointer;
2128 flex-grow: 2;
2129 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2130
2131 /* Objects in the "desktoponly" class are invisible on mobile */
2132 @media screen and (max-width: 600px) {
2133 .desktoponly {
2134 display: none;
2135
--- src/default.css
+++ src/default.css
@@ -2125,10 +2125,49 @@
2125 .Attacher .attach-controls .attach-add-button {
2126 padding: 0.5em 1em;
2127 cursor: pointer;
2128 flex-grow: 2;
2129 }
2130
2131 .animate-entrance {
2132 animation: slideFadeIn 0.25s ease-out forwards;
2133 transform-origin: top;
2134 overflow: hidden /*prevent content bleeding during expansion*/;
2135 }
2136 .animate-exit {
2137 animation: slideFadeOut 0.25s ease-in forwards;
2138 transform-origin: top;
2139 overflow: hidden;
2140 }
2141 @keyframes slideFadeIn {
2142 0% {
2143 opacity: 0;
2144 transform: translateY(-1em/*must match slideFadeOut*/);
2145 max-height: 0;
2146 }
2147 100% {
2148 opacity: 1;
2149 transform: translateY(0);
2150 max-height: 100em /*a value safely larger than the widget*/;
2151 }
2152 }
2153 @keyframes slideFadeOut {
2154 0% {
2155 opacity: 1;
2156 transform: translateY(0);
2157 max-height: 100em /*must match slideFadeIn*/;
2158 }
2159 100% {
2160 opacity: 0;
2161 transform: translateY(-1em/*must match slideFadeIn*/);
2162 max-height: 0;
2163 padding-top: 0;
2164 padding-bottom: 0;
2165 margin-top: 0;
2166 margin-bottom: 0;
2167 }
2168 }
2169
2170 /* Objects in the "desktoponly" class are invisible on mobile */
2171 @media screen and (max-width: 600px) {
2172 .desktoponly {
2173 display: none;
2174
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -190,22 +190,26 @@
190190
e.classList.add('hidden');
191191
}
192192
}
193193
194194
#removeRow(rowObj){
195
- rowObj.e.row.remove();
196
- this.#rows = this.#rows.filter(v=>v!==rowObj);
197
- this.#updateControls();
198
- this.#events.dispatchEvent(
199
- new CustomEvent('entry-removed',{
200
- detail: F.nu({
201
- type: 'entry-removed',
202
- row: rowObj,
203
- attacher: this
204
- })
205
- })
206
- );
195
+ const er = rowObj.e.row;
196
+ if( er.parentNode ){
197
+ this.#rows = this.#rows.filter(v=>v!==rowObj);
198
+ this.#updateControls();
199
+ er.classList.add('animate-exit');
200
+ er.addEventListener('animationend', ()=>er.remove(), {once: true});
201
+ this.#events.dispatchEvent(
202
+ new CustomEvent('entry-removed',{
203
+ detail: F.nu({
204
+ type: 'entry-removed',
205
+ row: rowObj,
206
+ attacher: this
207
+ })
208
+ })
209
+ );
210
+ }
207211
}
208212
209213
/**
210214
Removes all attachments and clears the error state.
211215
*/
@@ -398,10 +402,19 @@
398402
desc: eDesc,
399403
row: eRow,
400404
remove: eRemove
401405
});
402406
this.#e.body.append(eRow);
407
+ eRow.classList.add('animate-entrance');
408
+ requestAnimationFrame(() => {
409
+ eRow.scrollIntoView({
410
+ behavior: 'smooth',
411
+ block: 'nearest',
412
+ inline: 'nearest'
413
+ });
414
+ });
415
+
403416
this.#rows.push( rowObj );
404417
this.#updateControls();
405418
this.#events.dispatchEvent(
406419
new CustomEvent('entry-added',{
407420
detail: F.nu({
408421
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -190,22 +190,26 @@
190 e.classList.add('hidden');
191 }
192 }
193
194 #removeRow(rowObj){
195 rowObj.e.row.remove();
196 this.#rows = this.#rows.filter(v=>v!==rowObj);
197 this.#updateControls();
198 this.#events.dispatchEvent(
199 new CustomEvent('entry-removed',{
200 detail: F.nu({
201 type: 'entry-removed',
202 row: rowObj,
203 attacher: this
204 })
205 })
206 );
 
 
 
 
207 }
208
209 /**
210 Removes all attachments and clears the error state.
211 */
@@ -398,10 +402,19 @@
398 desc: eDesc,
399 row: eRow,
400 remove: eRemove
401 });
402 this.#e.body.append(eRow);
 
 
 
 
 
 
 
 
 
403 this.#rows.push( rowObj );
404 this.#updateControls();
405 this.#events.dispatchEvent(
406 new CustomEvent('entry-added',{
407 detail: F.nu({
408
--- src/fossil.attach.js
+++ src/fossil.attach.js
@@ -190,22 +190,26 @@
190 e.classList.add('hidden');
191 }
192 }
193
194 #removeRow(rowObj){
195 const er = rowObj.e.row;
196 if( er.parentNode ){
197 this.#rows = this.#rows.filter(v=>v!==rowObj);
198 this.#updateControls();
199 er.classList.add('animate-exit');
200 er.addEventListener('animationend', ()=>er.remove(), {once: true});
201 this.#events.dispatchEvent(
202 new CustomEvent('entry-removed',{
203 detail: F.nu({
204 type: 'entry-removed',
205 row: rowObj,
206 attacher: this
207 })
208 })
209 );
210 }
211 }
212
213 /**
214 Removes all attachments and clears the error state.
215 */
@@ -398,10 +402,19 @@
402 desc: eDesc,
403 row: eRow,
404 remove: eRemove
405 });
406 this.#e.body.append(eRow);
407 eRow.classList.add('animate-entrance');
408 requestAnimationFrame(() => {
409 eRow.scrollIntoView({
410 behavior: 'smooth',
411 block: 'nearest',
412 inline: 'nearest'
413 });
414 });
415
416 this.#rows.push( rowObj );
417 this.#updateControls();
418 this.#events.dispatchEvent(
419 new CustomEvent('entry-added',{
420 detail: F.nu({
421
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -372,20 +372,34 @@
372372
e.buttons.append(e.button.toggleHeader);
373373
}
374374
375375
}/*constructor*/
376376
377
- discard(){
377
+ close(){
378378
const e = this.#e.widget;
379
- if( e.parentNode ){
379
+ if( e?.parentNode ){
380
+ if( this.#opt.onclose instanceof Function ){
381
+ try{this.#opt.onclose();}
382
+ catch(e){
383
+ console.error("ForumPostEditor.onclose() threw:",e);
384
+ }
385
+ }
380386
//console.debug("FPE discarding", this);
381
- this.#clearDraft();
382
- e.remove();
383
- if( this.#opt.ondiscard instanceof Function ){
384
- this.#opt.ondiscard();
387
+ e.classList.add('animate-exit');
388
+ e.addEventListener('animationend', ()=>e.remove(), {once: true});
389
+ }
390
+ }
391
+
392
+ discard(){
393
+ if( this.#opt.ondiscard instanceof Function ){
394
+ try{this.#opt.ondiscard();}
395
+ catch(e){
396
+ console.error("ForumPostEditor.ondiscard() threw:",e);
385397
}
386398
}
399
+ this.#clearDraft();
400
+ this.close();
387401
}
388402
389403
/** This widget's top-most DOM element. */
390404
get widget(){
391405
return this.#e.widget;
@@ -410,13 +424,13 @@
410424
*/
411425
reportError(...msg){
412426
const e = this.#e.error;
413427
D.clearElement(e);
414428
if( msg.length ){
429
+ console.error('ForumPostEditor:',...msg);
415430
e.classList.remove('hidden');
416431
e.append(...msg);
417
- console.error('ForumPostEditor:',...msg);
418432
}else{
419433
e.classList.add('hidden');
420434
}
421435
}
422436
@@ -612,11 +626,11 @@
612626
});
613627
}
614628
if( this.#att ){
615629
this.#att.populateFormData(fd);
616630
}
617
- console.warn("Ready to submit",fd);
631
+ //console.warn("Ready to submit",fd);
618632
if( 0 ){
619633
this.#isWaiting = false;
620634
return;
621635
}
622636
const resp = window.fetch(F.repoUrl('forumajax_save'), {
@@ -635,11 +649,14 @@
635649
return;
636650
}
637651
if( 1 ){
638652
this.#clearDraft();
639653
if( this.#opt.onsubmit instanceof Function ){
640
- this.#opt.onsubmit(this);
654
+ try{this.#opt.onsubmit(this);}
655
+ catch(e){
656
+ console.error("ForumPostEditor.onsubmit() threw: ", e);
657
+ }
641658
}
642659
window.location = F.repoUrl('forumpost/'+j.uuid);
643660
}else{
644661
this.reportError(
645662
"Saving worked but we're ignoring it and staying here."
@@ -883,11 +900,18 @@
883900
const w = fpe.widget;
884901
w.style.borderTop = '1px dotted';
885902
//w.style.marginTop = '0.35em';
886903
/* Adding an "Editing..." <h3> here adds way too much space */
887904
ePost.append(w);
888
- w.scrollIntoView();
905
+ w.classList.add('animate-entrance');
906
+ requestAnimationFrame(() => {
907
+ w.scrollIntoView({
908
+ behavior: 'smooth',
909
+ block: 'nearest',
910
+ inline: 'nearest'
911
+ });
912
+ });
889913
};
890914
891915
const eForumNew = (
892916
document.body.classList.contains('cpage-forumnew')
893917
|| document.body.classList.contains('cpage-forume1')
894918
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -372,20 +372,34 @@
372 e.buttons.append(e.button.toggleHeader);
373 }
374
375 }/*constructor*/
376
377 discard(){
378 const e = this.#e.widget;
379 if( e.parentNode ){
 
 
 
 
 
 
380 //console.debug("FPE discarding", this);
381 this.#clearDraft();
382 e.remove();
383 if( this.#opt.ondiscard instanceof Function ){
384 this.#opt.ondiscard();
 
 
 
 
 
 
385 }
386 }
 
 
387 }
388
389 /** This widget's top-most DOM element. */
390 get widget(){
391 return this.#e.widget;
@@ -410,13 +424,13 @@
410 */
411 reportError(...msg){
412 const e = this.#e.error;
413 D.clearElement(e);
414 if( msg.length ){
 
415 e.classList.remove('hidden');
416 e.append(...msg);
417 console.error('ForumPostEditor:',...msg);
418 }else{
419 e.classList.add('hidden');
420 }
421 }
422
@@ -612,11 +626,11 @@
612 });
613 }
614 if( this.#att ){
615 this.#att.populateFormData(fd);
616 }
617 console.warn("Ready to submit",fd);
618 if( 0 ){
619 this.#isWaiting = false;
620 return;
621 }
622 const resp = window.fetch(F.repoUrl('forumajax_save'), {
@@ -635,11 +649,14 @@
635 return;
636 }
637 if( 1 ){
638 this.#clearDraft();
639 if( this.#opt.onsubmit instanceof Function ){
640 this.#opt.onsubmit(this);
 
 
 
641 }
642 window.location = F.repoUrl('forumpost/'+j.uuid);
643 }else{
644 this.reportError(
645 "Saving worked but we're ignoring it and staying here."
@@ -883,11 +900,18 @@
883 const w = fpe.widget;
884 w.style.borderTop = '1px dotted';
885 //w.style.marginTop = '0.35em';
886 /* Adding an "Editing..." <h3> here adds way too much space */
887 ePost.append(w);
888 w.scrollIntoView();
 
 
 
 
 
 
 
889 };
890
891 const eForumNew = (
892 document.body.classList.contains('cpage-forumnew')
893 || document.body.classList.contains('cpage-forume1')
894
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -372,20 +372,34 @@
372 e.buttons.append(e.button.toggleHeader);
373 }
374
375 }/*constructor*/
376
377 close(){
378 const e = this.#e.widget;
379 if( e?.parentNode ){
380 if( this.#opt.onclose instanceof Function ){
381 try{this.#opt.onclose();}
382 catch(e){
383 console.error("ForumPostEditor.onclose() threw:",e);
384 }
385 }
386 //console.debug("FPE discarding", this);
387 e.classList.add('animate-exit');
388 e.addEventListener('animationend', ()=>e.remove(), {once: true});
389 }
390 }
391
392 discard(){
393 if( this.#opt.ondiscard instanceof Function ){
394 try{this.#opt.ondiscard();}
395 catch(e){
396 console.error("ForumPostEditor.ondiscard() threw:",e);
397 }
398 }
399 this.#clearDraft();
400 this.close();
401 }
402
403 /** This widget's top-most DOM element. */
404 get widget(){
405 return this.#e.widget;
@@ -410,13 +424,13 @@
424 */
425 reportError(...msg){
426 const e = this.#e.error;
427 D.clearElement(e);
428 if( msg.length ){
429 console.error('ForumPostEditor:',...msg);
430 e.classList.remove('hidden');
431 e.append(...msg);
 
432 }else{
433 e.classList.add('hidden');
434 }
435 }
436
@@ -612,11 +626,11 @@
626 });
627 }
628 if( this.#att ){
629 this.#att.populateFormData(fd);
630 }
631 //console.warn("Ready to submit",fd);
632 if( 0 ){
633 this.#isWaiting = false;
634 return;
635 }
636 const resp = window.fetch(F.repoUrl('forumajax_save'), {
@@ -635,11 +649,14 @@
649 return;
650 }
651 if( 1 ){
652 this.#clearDraft();
653 if( this.#opt.onsubmit instanceof Function ){
654 try{this.#opt.onsubmit(this);}
655 catch(e){
656 console.error("ForumPostEditor.onsubmit() threw: ", e);
657 }
658 }
659 window.location = F.repoUrl('forumpost/'+j.uuid);
660 }else{
661 this.reportError(
662 "Saving worked but we're ignoring it and staying here."
@@ -883,11 +900,18 @@
900 const w = fpe.widget;
901 w.style.borderTop = '1px dotted';
902 //w.style.marginTop = '0.35em';
903 /* Adding an "Editing..." <h3> here adds way too much space */
904 ePost.append(w);
905 w.classList.add('animate-entrance');
906 requestAnimationFrame(() => {
907 w.scrollIntoView({
908 behavior: 'smooth',
909 block: 'nearest',
910 inline: 'nearest'
911 });
912 });
913 };
914
915 const eForumNew = (
916 document.body.classList.contains('cpage-forumnew')
917 || document.body.classList.contains('cpage-forume1')
918

Keyboard Shortcuts

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