fix(main): broaden anchor selector and show inline "can't anchor" note
findAnchorElement used a narrow selector ('button[name="train"],
a[href*="train"]') that often missed Torn's actual gym page DOM.
When it missed, applyMode fell through to dialog.setMode('free'),
snapping the dialog to the default bottom-right position — which the
user perceived as a 'bounce' when clicking 'Above training UI'.
Widen the selector to a priority-ordered candidate list and prefer the
form ancestor of any matched element. As a last-ditch, look for a form
inside the gym panel. This covers more of Torn's gym-page variations.
When the anchor selector still misses, do NOT snap to the default free
position. Instead, keep the dialog where it is, set anchorError on the
state, and let the dialog render an inline note so the user gets
visible feedback explaining what happened.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+37
-5
@@ -4,11 +4,34 @@ import { currentAttribute } from './dom.js';
|
|||||||
import { startRequestInterceptor } from './interceptor.js';
|
import { startRequestInterceptor } from './interceptor.js';
|
||||||
|
|
||||||
function findAnchorElement() {
|
function findAnchorElement() {
|
||||||
// Torn's training form is the element containing the Train button.
|
// Try several selectors in priority order. Torn's gym page structure
|
||||||
// Selector is best-effort; the Dialog will fall back if missing.
|
// varies; we cast a wide net and return the first match.
|
||||||
const btn = document.querySelector('button[name="train"], a[href*="train"]');
|
const candidates = [
|
||||||
if (!btn) return null;
|
'form[action*="train"]',
|
||||||
return btn.closest('form') || btn.parentElement;
|
'form.train-form',
|
||||||
|
'form[class*="train"]',
|
||||||
|
'[class*="train-button"]',
|
||||||
|
'button[class*="train"]',
|
||||||
|
'a[class*="train"]',
|
||||||
|
'button[name="train"]',
|
||||||
|
'a[href*="train"]',
|
||||||
|
];
|
||||||
|
for (const sel of candidates) {
|
||||||
|
const el = document.querySelector(sel);
|
||||||
|
if (el) {
|
||||||
|
// Prefer the form ancestor if the match is a button/link, since we
|
||||||
|
// want to anchor above the whole form, not just the button.
|
||||||
|
return el.closest('form') || el;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Last-ditch: any element inside the gym panel that looks like the
|
||||||
|
// training form.
|
||||||
|
const panel = document.querySelector('.gym, #gym, [class*="gym-"], [class*="Gym"]');
|
||||||
|
if (panel) {
|
||||||
|
const form = panel.querySelector('form');
|
||||||
|
if (form) return form;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function start() {
|
function start() {
|
||||||
@@ -40,6 +63,7 @@ function start() {
|
|||||||
let lastSnapshot = null;
|
let lastSnapshot = null;
|
||||||
let lastAttr = null;
|
let lastAttr = null;
|
||||||
let lastDelta = 0;
|
let lastDelta = 0;
|
||||||
|
let anchorError = null;
|
||||||
|
|
||||||
function snapshot() {
|
function snapshot() {
|
||||||
const a = currentAttribute();
|
const a = currentAttribute();
|
||||||
@@ -56,6 +80,7 @@ function start() {
|
|||||||
perTrain: lastDelta,
|
perTrain: lastDelta,
|
||||||
summary,
|
summary,
|
||||||
warn: store._saveDisabled ? 'saving disabled this session' : null,
|
warn: store._saveDisabled ? 'saving disabled this session' : null,
|
||||||
|
anchorError: anchorError,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,9 +101,16 @@ function start() {
|
|||||||
if (prefs.mode === 'anchored') dialog._positionAnchored(el.getBoundingClientRect());
|
if (prefs.mode === 'anchored') dialog._positionAnchored(el.getBoundingClientRect());
|
||||||
});
|
});
|
||||||
ro.observe(el);
|
ro.observe(el);
|
||||||
|
anchorError = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Anchor selector missed — don't snap to default, just keep current
|
||||||
|
// position and show a note.
|
||||||
|
anchorError = "Couldn't find the training form on this page.";
|
||||||
|
render();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
anchorError = null;
|
||||||
dialog.setMode('free');
|
dialog.setMode('free');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const STYLE = `
|
|||||||
}
|
}
|
||||||
.tat-modes button.active { background: #444; border-color: #888; }
|
.tat-modes button.active { background: #444; border-color: #888; }
|
||||||
.tat-warn { color: #c90; margin-top: 6px; font-size: 11px; }
|
.tat-warn { color: #c90; margin-top: 6px; font-size: 11px; }
|
||||||
|
.tat-anchor-err { color: #c90; margin-top: 6px; font-size: 11px; }
|
||||||
.tat-error { padding: 8px 0; color: #f88; }
|
.tat-error { padding: 8px 0; color: #f88; }
|
||||||
.tat-error button { margin-left: 8px; }
|
.tat-error button { margin-left: 8px; }
|
||||||
`;
|
`;
|
||||||
@@ -227,6 +228,7 @@ export class Dialog {
|
|||||||
<div class="tat-row"><span>ETA</span><span>${est.days > 0 ? `~ ${fmtFull(est.days)} days (${fmtDate(est.eta)})` : '—'}</span></div>
|
<div class="tat-row"><span>ETA</span><span>${est.days > 0 ? `~ ${fmtFull(est.days)} days (${fmtDate(est.eta)})` : '—'}</span></div>
|
||||||
<div class="tat-modes">${modes}</div>
|
<div class="tat-modes">${modes}</div>
|
||||||
${warn ? `<div class="tat-warn">⚠ ${esc(warn)}</div>` : ''}
|
${warn ? `<div class="tat-warn">⚠ ${esc(warn)}</div>` : ''}
|
||||||
|
${state.anchorError ? `<div class="tat-anchor-err">⚠ ${esc(state.anchorError)}</div>` : ''}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
this.root.querySelector('.tat-close').onclick = () => this.onClose && this.onClose();
|
this.root.querySelector('.tat-close').onclick = () => this.onClose && this.onClose();
|
||||||
|
|||||||
Reference in New Issue
Block a user