Fixing(Workaround) the SilverBullet/CodeMirror Mobile Keyboard drama - Disable Accidental Taps

Let’s face it: SilverBullet on mobile is a nightmare for reading. You try to scroll, and the keyboard jumps out like a jack-in-the-box. It’s annoying, it’s clunky, and I’m done with it.

I’ve built a script (together with Gemini) that forces the editor to stay in “ReadOnly” mode (not the real SilverBullet-ReadOnly, but a virtualized one) until you actually intend to write.

What it does:

  • Blocks Single Taps: Poking the text does nothing. No cursor, no keyboard, no jumping screen.
  • Scroll Freely: You can swipe and navigate without the UI losing its mind.
  • The Unlock: Double-tap or Long-press the editor to summon the keyboard. Once you click away, it locks itself again.

Why it works: We went nuclear. Instead of asking the editor to behave, we used a Capture Phase interceptor on #sb-editor. It catches tap signals at the window level and deletes them before the browser can trigger the “helpful” keyboard pop-up.

It turns your notes back into a document, not a minefield.

I added a command to trigger it: Mobile: Toggle Accidental Tap Shield"
and also included in the commets a disabled event listened if you want to enable it permanently or PageLoaded.

-- The Streamlined Handshake (v7) with Toggle Command
-- Supports both Double-Tap OR Long-Press to unlock.

function setupStreamlinedShield()
  -- If the shield is already globally marked as Active, don't inject again
  if js.window.sbStreamlinedActive then return end

  local scriptContent = [[
    (function() {
      let lastTap = 0;
      let holdTimer = null;
      const DOUBLE_TAP_THRESHOLD = 300;
      const LONG_PRESS_THRESHOLD = 500;
      let isUnlocked = false;

      const unlock = () => {
        isUnlocked = true;
        console.log("Jarvis: Perimeter disarmed. Editing enabled.");
      };

      const lock = () => {
        isUnlocked = false;
      };

      const handleCapture = (e) => {
        // Critical: If the user toggled the shield OFF, stop intercepting immediately
        if (window.sbShieldDisabledManually) return;

        const isEditor = e.target.closest('#sb-editor');
        const isUI = e.target.closest('.sb-actions, .sb-top-bar, .sb-sidebar, button, .menu-item');

        if (!isEditor || isUI) return;
        if (isUnlocked && document.activeElement.closest('#sb-editor')) return;

        const now = Date.now();

        if (e.type === 'touchstart') {
          if (now - lastTap < DOUBLE_TAP_THRESHOLD) {
            unlock();
          }
          lastTap = now;

          clearTimeout(holdTimer);
          holdTimer = setTimeout(() => {
            unlock();
            const el = document.querySelector('#sb-main .cm-content');
            if (el) {
              el.setAttribute('inputmode', 'text');
              el.focus();
            }
          }, LONG_PRESS_THRESHOLD);
        }

        if (e.type === 'touchend' || e.type === 'touchmove') {
          clearTimeout(holdTimer);
        }

        if (!isUnlocked) {
          const killZone = ['mousedown', 'mouseup', 'click', 'touchend'];
          if (killZone.includes(e.type)) {
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();
          }
        }
      };

      document.addEventListener('focusout', (e) => {
        if (e.target.closest('#sb-editor')) lock();
      }, true);

      ['touchstart', 'touchend', 'touchmove', 'mousedown', 'mouseup', 'click'].forEach(type => {
        window.addEventListener(type, handleCapture, { capture: true, passive: false });
      });

      window.sbStreamlinedActive = true;
      console.log("Jarvis: v7 Streamlined Shield is operational.");
    })();
  ]]

  local scriptEl = js.window.document.createElement("script")
  scriptEl.innerHTML = scriptContent
  js.window.document.body.appendChild(scriptEl)
end

command.define {
  name = "Mobile: Toggle Accidental Tap Shield",
  run = function() 
    if not js.window.sbShieldDisabledManually then
      -- Turn Shield OFF
      js.window.sbShieldDisabledManually = true
      editor.flashNotification("Shield DISARMED. Normal tapping restored.", "info")
    else
      -- Turn Shield ON
      js.window.sbShieldDisabledManually = false
      setupStreamlinedShield()
      editor.flashNotification("Shield ENGAGED. Double-tap/Hold to edit.", "info")
    end
  end
}

-- Auto-init on load uncomment following line to enable it on pageLoaded event.
-- event.listen {  name = "editor:pageLoaded",  run = function() setupStreamlinedShield() end }
4 Likes

Also added it to my Silverbullet-Libraries

Or you can install it through the Library Manager from my Repo

3 Likes

Wonderful work!

I’d really love to see this behavior built-in in SB @zef :slight_smile:

1 Like

Great stuff! I also would like to see this feature implemented in SB.

1 Like

i love this plugin! it really improves the experience on a mobile device.

one question though - i see that when if i tap a link to a page (e.g. [[page name]]), it works as expected. however, when the page link is generated via a query, it summons the keyboard. is there a way to prevent that as well?

for example, i regularly use a query like this to generate a list of pages that parent == "[[some page]]" in their frontmatter:

${template.each(
query[[from index.tag "page"
    where parent == "[[some page]]"
]],
templates.pageItem
)}

Let me ask you to see if I understood it correctly:
You want to click on a [[WikiLink]] or a [[PageRef]] which was generated by a query or inside a rendered table. So the click should follow that link, and not turn on markdown edit mode of the query? is this correct?

If so, I just pushed an Update to address this to my DisableAccidentalMobileTaps - library
So please give an update & test it an leave a feedback if the issue you've been talking about is solved?

. So the click should follow that link, and not turn on markdown edit mode of the query? is this correct?

yes, that's correct.

i just updated the plugin and tested it, and unfortunately the issue still persists. if it helps, tapping anything inside the "box" of the query output (refresh button for example, see the image below) summons the keyboard. the same happens with the linked mentions as well.

I think now I get it, so the issue wasn’t that the WikiLink pages didn’t work inside a query, or that the query went into edit mode. The issue is that after following the Link which is generated by a query the Keyboard pops up when you arrive at the linked page? Is this the issue you’re experiencing?

If yes I will look into this tomorrow because today it’s already too late.

The issue is that after following the Link which is generated by a query the Keyboard pops up when you arrive at the linked page? Is this the issue you’re experiencing?

partially, yes. the query generates a list of pages with their links and i tap one of them which takes me to that page, and then the keyboard pops up.

if i were to touch any of the three buttons on the upper right of the query output box (the image that i shared in the previous post), the same would happen as well.

If yes I will look into this tomorrow because today it’s already too late.

of course, take your time! thanks for taking a look at it!

Ok, I’ve pushed the Update, couldn’t leave it until tomorrow. try it now please. It should be fixed now.

just tested it and it works. thanks for the update!

1 Like