Button for scrolling to the last block of a page (macro with Kits)

(post updated to share clear instructions)

Instructions

  1. Follow @mentaloid’s instructions to set up Kits: Edit and run javascript code inside Logseq itself
  2. Create a page titled ScrollToLastBlock in Logseq.
  3. In a code block in that page, add the following code:
    const Kits = logseq.Module.Kits;
    
    const doClick = new MouseEvent('click', {
      view: window,
      bubbles: true,
      cancelable: true,
    });
    
    logseq.kits.scrolltolastblock = Kits.addClickEventToButton.bind(
      null,
      function onClickScrollToLastBlock(e) {
        const div = e.target.closest('.blocks-container > div > div').lastChild;
        div.scrollIntoView(); // to scroll only until div aligns with the bottom of the viewport, change to `div.scrollIntoView({ block: 'end' })`
        if (div.classList.contains('w-full')) {
          const link = div.querySelector(':scope > a');
          if (link) link.dispatchEvent(doClick);
          setTimeout(onClickScrollToLastBlock, 250, e);
        }
      }
    );
    
  4. Add the following in the :macros section of config.edn:
    :scrolltolastblock "<button class='kit eval' data-kit='scrolltolastblock'>► Scroll to last block</button>"}
    
  5. Add {{scrolltolastblock}} as a block at the top of a lengthy page, and the button will scroll the page all the way to the last block. It even works on the sidebar, by simulating a mouse click on the “More” link!

Many thanks to @mentaloid for help. Feedback and suggestions welcome.


(Previous original post is below, for reference):

I wanted an automatic way to scroll to the last block of a long page, and to get around Logseq’s lazy-loading, so I made a button to do it.

Using @mentaloid 's Edit and run javascript code inside Logseq itself gave me the tools to get something working.

After setting up Kits per the instructions, I made a page titled ScrollToLastBlock with the following in a code block:

const Kits = logseq.Module.Kits;

const getBlocks = (el) => el.querySelectorAll("div.ls-block");

const delay = (ms) => new Promise((res) => setTimeout(res, ms));

logseq.kits.scrolltolastblock = Kits.addClickEventToButton.bind(
  null,
  async function onClickScrollToLastBlock(e) {
    const block = e.target.closest("div.ls-block");
    const pageEl = block.closest(".page");
    let blocks = getBlocks(pageEl);
    let blockCount = blocks.length;
    let prevBlockCount;
    while (blockCount && blockCount !== prevBlockCount) {
      const lastBlock = blocks[blockCount - 1];
      lastBlock.scrollIntoView({ block: "end" });
      await delay(250);
      prevBlockCount = blockCount;
      blocks = getBlocks(pageEl);
      blockCount = blocks.length;
    }
  }
);

and added the following in the :macros section of config.edn:

:scrolltolastblock "<button class='kit eval' data-kit='scrolltolastblock'>► Scroll to last block</button>"}

Then by adding {{scrolltolastblock}} at the top of a lengthy page, I have a button that scrolls all the way down for me, and Logseq lazy loads as it scrolls.

Any suggestions on improvements would be welcome!

It doesn’t work for long pages in the sidebar, because those require clicking the “more” link to load more blocks (sidebar doesn’t automatically lazy load).

I tried using the API to access the last block, which I found I could do, but I ran into trouble using the API to scroll to it. Relevant experimental section of code:

    // api strategy: don't bother with unless scroll_to_block_in_page can work, but it doesn't.
    // at top, add:
    const LS = logseq.api;
    // ...
    const page = LS.get_current_page();
    let childBlocks = LS.get_page_blocks_tree(page.uuid);
    let hasChildren, lastBlock;
    do {
      lastBlock = childBlocks[childBlocks.length - 1];
      childBlocks = lastBlock.children;
    } while (lastBlock.children.length);
    LS.scroll_to_block_in_page(page.name, lastBlock.uuid); //doesn't work

scrollToBlockInPage returned an error, using the underscore version scroll_to_block_in_page(). If anyone has suggestions on how to get that working instead of the purely DOM method I’m using instead, I’m all ears.

2 Likes

Simplified code for scrolling to the end of the page:

const Kits = logseq.Module.Kits;

logseq.kits.scrolltolastblock = Kits.addClickEventToButton.bind(
  null, function onClickScrollToLastBlock(e, top = 0){
    const div = document.getElementById("main-content-container");
    const height = div.scrollHeight;
    if (top >= height) return;

    div.scrollTop = height;
    setTimeout(onClickScrollToLastBlock, 250, e, height);
  }
);
4 Likes

Thank you, and thanks for moving this topic to the proper category.

For scrolling all the way to the bottom of the current page, including through linked and/or unlinked references, your code is indeed simpler and more elegant.

However, the goal for my button is to scroll to the last block of the page, but to stop before reaching the linked and/or unlinked references sections. (One use case among many might be to add something to a long list.)

With that goal in mind, I’d be curious if you have suggestions for:

  • doing it in a simpler/better way
  • achieving it with the API like I attempted to do (but scrollToBlockInPage failed, and without that working, there didn’t seem to be an advantage over using the DOM)
  • scrolling until the last block is centered (if enough content below allows) in the viewport (not particular important, though)
  • getting this to work in pages in the sidebar, where lazy-loading isn’t automatic (simulate mouseclick on the “More” link that loads more blocks?)

fyi, I’ve updated my previously shared code with some slight optimizations (don’t count blocks twice per loop, don’t define unneeded Modules).

3 Likes

On this topic, using the DOM seems… iffy. The only identifying DOM features of the “More” link are that it’s

  • the last sibling of the top-level blocks
  • has the following HTML
    <div class="w-full p-4">
      <a class="fade-link text-link font-bold text-sm">More</a>
    </div>
    

I’m wary of simulating a mouseclick based on some of those attributes, but I guess it could work. Perhaps there’s a method using the API?

On this topic, I found this issue: [API]: logseq.Editor.scrollToBlockInPage does not work on long pages. Although I’m not sure whether, if the issue is fixed, the method will stop producing an error, since the console complained that scroll_to_block_in_page(<page_name>, <block_uuid>) was not a function.

2 Likes

For the last block, try this:

const Kits = logseq.Module.Kits;

logseq.kits.scrolltolastblock = Kits.addClickEventToButton.bind(
  null, function onClickScrollToLastBlock(e){
    const div = document.querySelector(".blocks-container > div > div").lastChild;
    div.scrollIntoView();
    if (div.classList.contains("w-full")) setTimeout(onClickScrollToLastBlock, 250, e);
  }
);
3 Likes

This is brilliant, thank you! Just finding the last child of the containing element, then using setTimeOut() to re-run the function and the button event simplifies things beautifully.

I was able to use your code to update it to be selective for whether the button is clicked in the sidebar or in the main view:

  • Instead of using document.querySelector(), use e.target.closest() to get the correct .blocks-container
  • If the last child div.w-full contains a link (the “More” link in the sidebar), simulate a mouse click

I also added a comment on using scrollIntoView({ block: 'end' }), for users who may want the scroll only to proceed until the block is fully visible.

Here’s the code. Now working in the sidebar, including when the page is open both in main view and sidebar!

const Kits = logseq.Module.Kits;

const doClick = new MouseEvent('click', {
  view: window,
  bubbles: true,
  cancelable: true,
});

logseq.kits.scrolltolastblock = Kits.addClickEventToButton.bind(
  null,
  function onClickScrollToLastBlock(e) {
    const div = e.target.closest('.blocks-container > div > div').lastChild;
    div.scrollIntoView(); // to scroll only until div aligns with the bottom of the viewport, change to `div.scrollIntoView({ block: 'end' })`
    if (div.classList.contains('w-full')) {
      const link = div.querySelector(':scope > a');
      if (link) link.dispatchEvent(doClick);
      setTimeout(onClickScrollToLastBlock, 250, e);
    }
  }
);
4 Likes

Can you turn this into a plugin please? :slight_smile:

Sorry, I have no idea how to create plugins!