// src/editor/extensions/firefox-android-backspace.ts
/**
* Workaround for a CM6 gap on Firefox Android.
*
* CM6 has special handling for Chrome Android's IME-routed backspace
* (the PendingKeys / delayAndroidKey mechanism in @codemirror/view),
* but no equivalent for Firefox Android. When Firefox Android routes
* Backspace through the IME pipeline (keydown: Process, keyCode 229),
* CM6 falls back to watching for DOM mutations. But backspace at the
* start of a line (
) produces no DOM
* mutation, so CM6 sees nothing and drops the event.
*
* The fix mimics exactly what CM6 does for Chrome Android:
* - On beforeinput(deleteContentBackward), call delayAndroidKey('Backspace', 8)
* - Do NOT preventDefault — let Firefox apply its native backspace
* - The delayedAndroidKey lock suppresses onSelectionChange and flush
* until the next RAF
* - The RAF calls flush() to read whatever DOM mutations Firefox made
* - If flush() found no mutations (empty-line case) AND force is true,
* CM6 dispatches a synthetic keydown that triggers deleteCharBackward
* from defaultKeymap
*
* This is the same proven code path Chrome Android uses, just triggered
* for Firefox Android too. See @codemirror/view input.ts PendingKeys
* and domobserver.ts delayAndroidKey.
*
* Uses internal CM6 API (view.observer.delayAndroidKey). Guarded with
* runtime checks so a future CM6 refactor degrades gracefully.
* Tested against @codemirror/view 6.41.1.
*/
import { type Extension } from '@codemirror/state'
import { EditorView } from '@codemirror/view'
/** Detect Firefox Android using the same UA signals CM6 uses internally. */
const isFirefoxAndroid = typeof navigator !== 'undefined'
&& /Firefox/.test(navigator.userAgent)
&& /Android/.test(navigator.userAgent)
export function firefoxAndroidBackspace(): Extension {
if (!isFirefoxAndroid) return []
return EditorView.domEventHandlers({
beforeinput(event, view) {
if (event.inputType !== 'deleteContentBackward') return false
const observer = (view as any).observer
if (typeof observer?.delayAndroidKey !== 'function') return false
observer.delayAndroidKey('Backspace', 8)
// Return false: do NOT consume the event. Let Firefox apply its
// native backspace to the DOM, and let CM6's own beforeinput handler
// run too (it won't do anything for Firefox since its PendingKeys
// check is gated on browser.chrome && browser.android).
return false
},
})
}