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.
Commit
cb1e0213b3941c1062fe362c9680eca81221718cd807e49a02feaed594954c03
Parent
411a3c5da2b2445…
1 file changed
+57
-4
+57
-4
| --- src/fossil.page.forumpost.js | ||
| +++ src/fossil.page.forumpost.js | ||
| @@ -7,10 +7,28 @@ | ||
| 7 | 7 | /* JS code for /forumpost and friends. Requires fossil.dom |
| 8 | 8 | and can optionally use fossil.pikchr. */ |
| 9 | 9 | const P = F.page, D = F.dom; |
| 10 | 10 | |
| 11 | 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 | + | |
| 12 | 30 | /** |
| 13 | 31 | A WIP forum post editor widget for both new posts and responses. |
| 14 | 32 | */ |
| 15 | 33 | class ForumPostEditor { |
| 16 | 34 | /* Options */ |
| @@ -390,10 +408,14 @@ | ||
| 390 | 408 | } |
| 391 | 409 | } |
| 392 | 410 | //console.debug("FPE discarding", this); |
| 393 | 411 | e.classList.add('animate-exit'); |
| 394 | 412 | e.addEventListener('animationend', ()=>e.remove(), {once: true}); |
| 413 | + dummyPadding.refs.delete(this); | |
| 414 | + if( 0===dummyPadding.refs.size ){ | |
| 415 | + dummyPadding.remove(); | |
| 416 | + } | |
| 395 | 417 | } |
| 396 | 418 | } |
| 397 | 419 | |
| 398 | 420 | /* |
| 399 | 421 | ** Discards any draft edits then calls close(). If an ondiscard |
| @@ -412,10 +434,14 @@ | ||
| 412 | 434 | this.close(); |
| 413 | 435 | } |
| 414 | 436 | |
| 415 | 437 | /** This widget's top-most DOM element. */ |
| 416 | 438 | get widget(){ |
| 439 | + if( !dummyPadding.parentElement ){ | |
| 440 | + document.body.append(dummyPadding); | |
| 441 | + } | |
| 442 | + dummyPadding.refs.add(this); | |
| 417 | 443 | return this.#e.widget; |
| 418 | 444 | } |
| 419 | 445 | |
| 420 | 446 | get editorContent(){ |
| 421 | 447 | /* We wrap access to the editor's contents in a getter/setter so |
| @@ -563,17 +589,35 @@ | ||
| 563 | 589 | return t; |
| 564 | 590 | }); |
| 565 | 591 | } |
| 566 | 592 | |
| 567 | 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); | |
| 568 | 602 | const preview = this.#e.preview; |
| 569 | - D.clearElement(preview); | |
| 603 | + const childs = [...preview.childNodes]; | |
| 570 | 604 | D.parseHtml(preview, rawHtml); |
| 605 | + D.remove(childs); | |
| 606 | + dummy.remove(); | |
| 607 | + //preview.style.removeProperty('height'); | |
| 571 | 608 | if(F.pikchr && 'text/x-markdown'===this.mimetype){ |
| 572 | 609 | F.pikchr.addSrcView( |
| 573 | 610 | preview.querySelectorAll('svg.pikchr') |
| 574 | 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(); | |
| 575 | 619 | } |
| 576 | 620 | } |
| 577 | 621 | |
| 578 | 622 | async #preview(){ |
| 579 | 623 | if( this.#isWaiting ) return; |
| @@ -581,19 +625,28 @@ | ||
| 581 | 625 | if( e.preview !== this.#activeTab ){ |
| 582 | 626 | this.#tabs.switchToTab(e.preview); |
| 583 | 627 | /* Will recurse into here */ |
| 584 | 628 | return; |
| 585 | 629 | } |
| 586 | - this.#isWaiting = true; | |
| 587 | - D.clearElement(e.preview); | |
| 588 | 630 | const content = this.editorContent.trim(); |
| 589 | 631 | //console.debug("content to preview", content); |
| 590 | 632 | if( !content ){ |
| 591 | 633 | return; |
| 592 | 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; | |
| 593 | 647 | D.disable(this.#toDisable, e.button.submit); |
| 594 | - e.preview.textContent = "Fetching preview..."; | |
| 595 | 648 | this.#fetchPreview(content) |
| 596 | 649 | .then((c)=>{ |
| 597 | 650 | this.#setPreviewContent(c); |
| 598 | 651 | D.enable(e.button.submit); |
| 599 | 652 | }) |
| 600 | 653 |
| --- 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 |