Giving a Userscript Gestures While Blocking the Host Page
Taking Over More Than the DOM
Tango Explorer is a userscript that replaces Tango’s interface with a small stream browser built for my phone. Replacing the visible DOM was not enough. The original application continued registering timers, observers, and event listeners that could interfere with the replacement.
The userscript therefore sanitizes the environment early. Among other things,
it patches EventTarget.prototype.addEventListener and ignores event types the
host uses heavily:
const noisyEvents = [
"scroll",
"mousemove",
"resize",
"touchmove",
"touchstart",
];
EventTarget.prototype.addEventListener = function (type, listener, options) {
if (noisyEvents.includes(type)) return;
return originalAddEventListener.call(this, type, listener, options);
};
That stopped the original page from reclaiming touch and scroll behavior. It
also created an obvious problem: my own gesture controller uses touchstart
and touchmove.
Calling the patched method from my application silently registered nothing.
Removing touch events from noisyEvents made the controls work, but restored
the host application’s access at the same time. Both interfaces could then
interpret the same finger movement.
Preserve the Capability Before Revoking It
The useful solution was to save the native method before replacing it:
const originalAddEventListener =
EventTarget.prototype.addEventListener;
The environment setup returns that reference to the application. It is passed through the video manager into each stream unit and finally into the gesture controller.
The gesture controller never calls the patched public method for its touch events. It calls the preserved capability directly:
listen.call(container, "touchstart", onTouchStart);
listen.call(
container,
"touchmove",
onTouchMove,
{ passive: false },
);
The distinction is now deliberate:
Host page code
→ EventTarget.prototype.addEventListener
→ patched policy
→ noisy touch events rejected
Tango Explorer
→ saved originalAddEventListener
→ native browser implementation
→ gestures registered
The userscript does not need to recognize every host callback or continuously remove listeners after they appear. It changes the capability available to untrusted page code while retaining the original capability for itself.
Why Dependency Injection Is Appropriate Here
Passing a built-in function through several constructors can look excessive.
Reading the global EventTarget.prototype.addEventListener in the gesture
controller would be shorter, but by that point the global has already been
patched.
The explicit parameter documents which side of the boundary the controller belongs to:
new GestureController(
container,
callbacks,
elements,
originalAddEventListener,
peekCallbacks,
);
It also prevents later code from accidentally switching back to the patched method. The controller receives exactly the authority it needs: permission to register its own touch handlers.
This pattern also applies to timers. Tango Explorer preserves the original
setTimeout and setInterval before clearing the host’s scheduled work. Code
that belongs to the replacement application uses the preserved functions.
The Gesture Boundary
Once the handlers are registered through the private path, the controller classifies a touch sequence only once:
- movements inside a small dead zone remain undecided;
- the first dominant axis locks the gesture;
- a horizontal gesture beginning near the left edge becomes Back;
- other horizontal gestures show or hide controls;
- vertical gestures navigate between streams.
touchmove is registered with { passive: false }, allowing the selected
gesture to call preventDefault() and stop Safari or the host page from
interpreting it simultaneously.
The edge zone and dead zone are ratios of the application width rather than fixed pixels. The gesture therefore keeps roughly the same physical character across different phone widths.
What This Is—and Is Not
This is not a general security sandbox. Code already holding references to the
native method could bypass the patch, and the host page still shares the same
JavaScript realm. Tango Explorer is intentionally specific: it runs at
document-start, captures the capabilities first, and removes the particular
host behaviors that conflict with its replacement UI.
Within that boundary, the pattern has remained useful:
Save the browser capability before restricting the environment. Give the replacement application the saved capability explicitly; leave page code with the restricted global.
A userscript that takes over a hostile DOM also needs to decide who is allowed to participate in the event system. Preserving the original method turns a blunt global patch into a controlled boundary.