-
Notifications
You must be signed in to change notification settings - Fork 0
JavaScript DOM and Events
🛟 Need help or found a bug? Get support at support.doodesch.de/sideload.
Jint with every experimental feature on: ES2015 through ES2024. Classes with #private fields, optional
chaining, nullish coalescing, destructuring, template literals, generators, async/await syntax, spread,
replaceChildren, at(-1). Nothing is transpiled or polyfilled - write modern JavaScript.
document, s1, console, fetch, Promise, setTimeout, setInterval, clearTimeout, clearInterval.
That is the whole list. There is no window, no navigator, no localStorage - s1.storage replaces the
last one. Timers are driven by the mod's update loop rather than by threads, so they fire on the Unity main
thread and never race with your C#.
-
250 ms budget per handler. A runaway loop is one hitched frame, not a hung game; the handler is aborted
and the error is logged with
file:line. - A page is refused before it is rendered if it exceeds 20000 elements or 200 nesting levels. Styling, tree building and painting are each recursive, and blowing the managed stack takes the process down before an error page could be shown.
- One script engine per page. A reload drops it, so JS state and every listener start over - deliberately, because the script itself may be what changed.
document.getElementById('entry')
document.querySelector('.thread.on')
document.querySelectorAll('.bubble')
document.createElement('div')
document.addEventListener('back', fn) // binds at <body>el.textContent el.className el.id el.value
el.appendChild(child) el.replaceChildren() el.remove()
el.setAttribute(n, v) el.getAttribute(n) el.removeAttribute(n)
el.classList.add/remove/toggle/contains
el.style.setProperty(name, value)
el.querySelector(sel) el.querySelectorAll(sel)
el.addEventListener(type, fn)
el.scrollToEnd() // the one method the host adds and a browser does not havereplaceChildren() takes no arguments - it clears the element. Append the new children yourself.
Every mutation goes through these wrappers, so the renderer marks the subtree dirty itself and coalesces a frame's worth of changes into one rebuild. You never call a render function.
Five types are dispatched, and no others. pointerdown, wheel, change, focus, blur, keyup and the
rest do not fire, however plausible the name - registering them is silently useless.
| Event | Where | Carries | Raised when |
|---|---|---|---|
click |
the element | - | the player clicks a wired element |
input |
the control | e.value |
the text in an input/textarea changed |
keydown |
the control |
e.key === 'Enter', e.value
|
Enter in a single-line field. Enter only |
back |
document |
e.source, cancellable |
right-click or Escape |
orientationchange |
document | e.value |
the phone turned, after the page was laid out again |
Events bubble to the document in registration order per node; stopPropagation() ends the walk. The event object
carries type, target, currentTarget, value, key, source, defaultPrevented, preventDefault() and
stopPropagation().
A hit target is only wired on elements that a state rule targets, that are button/a/input/textarea, or
that the script has a click listener on. Everything else lets the pointer straight through, which is what
keeps a page of forty rows from being forty raycast targets.
Both mean back, exactly as they do in the vanilla apps - right-click steps out of a conversation before it closes Messages. Taking the event is what keeps your app open:
document.addEventListener('back', (e) => {
if (!deepInside()) return; // nothing to go back to, so Sideload closes the app
e.preventDefault();
goUpOneLevel();
});Not listening is fine and closes the app, which is the right behaviour for a single-screen app. An app with internal navigation that does not listen traps the player one level deep, so if you have panes, tabs or a detail view, handle this.
Check the orientation in that handler. In landscape a two-pane app usually shows both panes, so there is nothing to step back from and the press must close the app:
document.addEventListener('back', (e) => {
if (s1.orientation !== 'portrait' || pane !== 'detail') return;
e.preventDefault();
showList();
});Without that check the app silently switches a pane nobody can see and stops closing at all.
e.source is "rightClick" or "escape" for the rare page that must tell them apart. Most should not.
@media moves boxes. It cannot decide which pane the player should land on after a turn - that is a question
about what they were just looking at, and only your script knows:
document.addEventListener('orientationchange', (e) => {
if (e.value === 'portrait') showDetail(); // landscape had the detail on screen, so keep showing it
render();
});It fires after the page has been laid out at the new shape, so reading sizes in it is safe, and a change you make gets one more rebuild on the next tick.
console.log, .info, .warn, .error. Output goes to the MelonLoader log prefixed with your app id, and to
an attached Chrome DevTools console. Errors are also kept for the F9 overlay.
Build an element in one expression. Almost every render line needs this shape:
const el = (tag, className, text) => {
const node = document.createElement(tag);
if (className) node.className = className;
if (text != null) node.textContent = String(text);
return node;
};Bind per iteration, not after the loop. for (const x of xs) binds x per iteration, so a handler closes
over that item rather than the loop's last one. A var loop does not.
Do not call scrollToEnd() on every render. The host captures and restores scroll offsets across a rebuild,
so doing nothing keeps the reader's place. Call it only when the thing being read actually grew - remember the
identity and item count you last pinned for, and return early when neither moved.