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 }
2 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