Fossil SCM

Add some dummy padding to the bottom of the page while a forum editor is open to help reduce jumping-around of the UI when previewing, especially for the bottom-most post in a thread. This is a workaround, not a fix. Fix an order-of-operations bug which caused the Reply button state to get out of sync if the content to preview was empty.

stephan 2026-06-10 08:36 UTC forum-editor-2026
Commit cb1e0213b3941c1062fe362c9680eca81221718cd807e49a02feaed594954c03
1 file changed +57 -4
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -7,10 +7,28 @@
77
/* JS code for /forumpost and friends. Requires fossil.dom
88
and can optionally use fossil.pikchr. */
99
const P = F.page, D = F.dom;
1010
1111
let idCounter = 0;
12
+
13
+ /*
14
+ The problem: when previewing the bottom-most post of a thread, the
15
+ preview widget's size changes cause the page to scroll
16
+ unpredictably as the bottom boundary of the page moves. A weird
17
+ workaround (not invented here) is to add dummy blank padding to
18
+ the page to allow the preview widget to grow and shrink without
19
+ (usually) scrolling, but whether it does so really depends on its
20
+ size.
21
+
22
+ */
23
+ const dummyPadding = D.div();
24
+ dummyPadding.style.height = '100em';
25
+ /* Keep track of ForumPostEditor instances so we can remove this
26
+ padding when none are active. */
27
+ dummyPadding.refs = new Set();
28
+ F.dummyPadding = dummyPadding /* only for debugging */;
29
+
1230
/**
1331
A WIP forum post editor widget for both new posts and responses.
1432
*/
1533
class ForumPostEditor {
1634
/* Options */
@@ -390,10 +408,14 @@
390408
}
391409
}
392410
//console.debug("FPE discarding", this);
393411
e.classList.add('animate-exit');
394412
e.addEventListener('animationend', ()=>e.remove(), {once: true});
413
+ dummyPadding.refs.delete(this);
414
+ if( 0===dummyPadding.refs.size ){
415
+ dummyPadding.remove();
416
+ }
395417
}
396418
}
397419
398420
/*
399421
** Discards any draft edits then calls close(). If an ondiscard
@@ -412,10 +434,14 @@
412434
this.close();
413435
}
414436
415437
/** This widget's top-most DOM element. */
416438
get widget(){
439
+ if( !dummyPadding.parentElement ){
440
+ document.body.append(dummyPadding);
441
+ }
442
+ dummyPadding.refs.add(this);
417443
return this.#e.widget;
418444
}
419445
420446
get editorContent(){
421447
/* We wrap access to the editor's contents in a getter/setter so
@@ -563,17 +589,35 @@
563589
return t;
564590
});
565591
}
566592
567593
#setPreviewContent(rawHtml){
594
+ /**
595
+ Append the new content then remove the old, to help reduce
596
+ jumping-around of the UI if the preview is cleared then
597
+ repopulated.
598
+ */
599
+ const dummy = D.div();
600
+ dummy.style.height = '50em';
601
+ document.body.append(dummy);
568602
const preview = this.#e.preview;
569
- D.clearElement(preview);
603
+ const childs = [...preview.childNodes];
570604
D.parseHtml(preview, rawHtml);
605
+ D.remove(childs);
606
+ dummy.remove();
607
+ //preview.style.removeProperty('height');
571608
if(F.pikchr && 'text/x-markdown'===this.mimetype){
572609
F.pikchr.addSrcView(
573610
preview.querySelectorAll('svg.pikchr')
574611
);
612
+ }
613
+ if( 0 /* This isn't doing what is desired */
614
+ && !F.dom.isElementKindaInViewport(preview, true) ){
615
+ /* On the bottom-most post, these widgets sometimes
616
+ end up off-screen */
617
+ //F.dom.scrollChildIntoView(preview);
618
+ preview.scrollIntoView();
575619
}
576620
}
577621
578622
async #preview(){
579623
if( this.#isWaiting ) return;
@@ -581,19 +625,28 @@
581625
if( e.preview !== this.#activeTab ){
582626
this.#tabs.switchToTab(e.preview);
583627
/* Will recurse into here */
584628
return;
585629
}
586
- this.#isWaiting = true;
587
- D.clearElement(e.preview);
588630
const content = this.editorContent.trim();
589631
//console.debug("content to preview", content);
590632
if( !content ){
591633
return;
592634
}
635
+ if( 0
636
+ && !e.preview.firstElementChild ){
637
+ /* On an initial first preview, inherit the editor's height to
638
+ reduce jumping-around of the UI. */
639
+ if( 0 /* does not work: height of the editor is "auto" */ ){
640
+ const c = window.getComputedStyle(e.editor/*tabEdit*/);
641
+ e.preview.style.height = c.height;
642
+ }else{
643
+ e.preview.style.height = '20em';
644
+ }
645
+ }
646
+ this.#isWaiting = true;
593647
D.disable(this.#toDisable, e.button.submit);
594
- e.preview.textContent = "Fetching preview...";
595648
this.#fetchPreview(content)
596649
.then((c)=>{
597650
this.#setPreviewContent(c);
598651
D.enable(e.button.submit);
599652
})
600653
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -7,10 +7,28 @@
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 */
@@ -390,10 +408,14 @@
390 }
391 }
392 //console.debug("FPE discarding", this);
393 e.classList.add('animate-exit');
394 e.addEventListener('animationend', ()=>e.remove(), {once: true});
 
 
 
 
395 }
396 }
397
398 /*
399 ** Discards any draft edits then calls close(). If an ondiscard
@@ -412,10 +434,14 @@
412 this.close();
413 }
414
415 /** This widget's top-most DOM element. */
416 get widget(){
 
 
 
 
417 return this.#e.widget;
418 }
419
420 get editorContent(){
421 /* We wrap access to the editor's contents in a getter/setter so
@@ -563,17 +589,35 @@
563 return t;
564 });
565 }
566
567 #setPreviewContent(rawHtml){
 
 
 
 
 
 
 
 
568 const preview = this.#e.preview;
569 D.clearElement(preview);
570 D.parseHtml(preview, rawHtml);
 
 
 
571 if(F.pikchr && 'text/x-markdown'===this.mimetype){
572 F.pikchr.addSrcView(
573 preview.querySelectorAll('svg.pikchr')
574 );
 
 
 
 
 
 
 
575 }
576 }
577
578 async #preview(){
579 if( this.#isWaiting ) return;
@@ -581,19 +625,28 @@
581 if( e.preview !== this.#activeTab ){
582 this.#tabs.switchToTab(e.preview);
583 /* Will recurse into here */
584 return;
585 }
586 this.#isWaiting = true;
587 D.clearElement(e.preview);
588 const content = this.editorContent.trim();
589 //console.debug("content to preview", content);
590 if( !content ){
591 return;
592 }
 
 
 
 
 
 
 
 
 
 
 
 
593 D.disable(this.#toDisable, e.button.submit);
594 e.preview.textContent = "Fetching preview...";
595 this.#fetchPreview(content)
596 .then((c)=>{
597 this.#setPreviewContent(c);
598 D.enable(e.button.submit);
599 })
600
--- src/fossil.page.forumpost.js
+++ src/fossil.page.forumpost.js
@@ -7,10 +7,28 @@
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 /*
14 The problem: when previewing the bottom-most post of a thread, the
15 preview widget's size changes cause the page to scroll
16 unpredictably as the bottom boundary of the page moves. A weird
17 workaround (not invented here) is to add dummy blank padding to
18 the page to allow the preview widget to grow and shrink without
19 (usually) scrolling, but whether it does so really depends on its
20 size.
21
22 */
23 const dummyPadding = D.div();
24 dummyPadding.style.height = '100em';
25 /* Keep track of ForumPostEditor instances so we can remove this
26 padding when none are active. */
27 dummyPadding.refs = new Set();
28 F.dummyPadding = dummyPadding /* only for debugging */;
29
30 /**
31 A WIP forum post editor widget for both new posts and responses.
32 */
33 class ForumPostEditor {
34 /* Options */
@@ -390,10 +408,14 @@
408 }
409 }
410 //console.debug("FPE discarding", this);
411 e.classList.add('animate-exit');
412 e.addEventListener('animationend', ()=>e.remove(), {once: true});
413 dummyPadding.refs.delete(this);
414 if( 0===dummyPadding.refs.size ){
415 dummyPadding.remove();
416 }
417 }
418 }
419
420 /*
421 ** Discards any draft edits then calls close(). If an ondiscard
@@ -412,10 +434,14 @@
434 this.close();
435 }
436
437 /** This widget's top-most DOM element. */
438 get widget(){
439 if( !dummyPadding.parentElement ){
440 document.body.append(dummyPadding);
441 }
442 dummyPadding.refs.add(this);
443 return this.#e.widget;
444 }
445
446 get editorContent(){
447 /* We wrap access to the editor's contents in a getter/setter so
@@ -563,17 +589,35 @@
589 return t;
590 });
591 }
592
593 #setPreviewContent(rawHtml){
594 /**
595 Append the new content then remove the old, to help reduce
596 jumping-around of the UI if the preview is cleared then
597 repopulated.
598 */
599 const dummy = D.div();
600 dummy.style.height = '50em';
601 document.body.append(dummy);
602 const preview = this.#e.preview;
603 const childs = [...preview.childNodes];
604 D.parseHtml(preview, rawHtml);
605 D.remove(childs);
606 dummy.remove();
607 //preview.style.removeProperty('height');
608 if(F.pikchr && 'text/x-markdown'===this.mimetype){
609 F.pikchr.addSrcView(
610 preview.querySelectorAll('svg.pikchr')
611 );
612 }
613 if( 0 /* This isn't doing what is desired */
614 && !F.dom.isElementKindaInViewport(preview, true) ){
615 /* On the bottom-most post, these widgets sometimes
616 end up off-screen */
617 //F.dom.scrollChildIntoView(preview);
618 preview.scrollIntoView();
619 }
620 }
621
622 async #preview(){
623 if( this.#isWaiting ) return;
@@ -581,19 +625,28 @@
625 if( e.preview !== this.#activeTab ){
626 this.#tabs.switchToTab(e.preview);
627 /* Will recurse into here */
628 return;
629 }
 
 
630 const content = this.editorContent.trim();
631 //console.debug("content to preview", content);
632 if( !content ){
633 return;
634 }
635 if( 0
636 && !e.preview.firstElementChild ){
637 /* On an initial first preview, inherit the editor's height to
638 reduce jumping-around of the UI. */
639 if( 0 /* does not work: height of the editor is "auto" */ ){
640 const c = window.getComputedStyle(e.editor/*tabEdit*/);
641 e.preview.style.height = c.height;
642 }else{
643 e.preview.style.height = '20em';
644 }
645 }
646 this.#isWaiting = true;
647 D.disable(this.#toDisable, e.button.submit);
 
648 this.#fetchPreview(content)
649 .then((c)=>{
650 this.#setPreviewContent(c);
651 D.enable(e.button.submit);
652 })
653

Keyboard Shortcuts

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