Compare commits
7 Commits
95c655c24c
...
e44bf2b3c9
| Author | SHA1 | Date | |
|---|---|---|---|
| e44bf2b3c9 | |||
| b626fb7d41 | |||
| c6de810417 | |||
| ca83996c6e | |||
| a1e79ac801 | |||
| 7fddd84b6a | |||
| 2ed25c14de |
+10
-17
@@ -4,20 +4,20 @@ import { currentAttribute } from './dom.js';
|
|||||||
import { startRequestInterceptor } from './interceptor.js';
|
import { startRequestInterceptor } from './interceptor.js';
|
||||||
|
|
||||||
function findAnchorElement() {
|
function findAnchorElement() {
|
||||||
// Try several selectors in priority order. Torn's gym page renders the
|
// Return the element to insert the dialog BEFORE in the DOM.
|
||||||
// training UI as <ul class="properties___HASH"> (the list of attribute
|
// The user wants the dialog between the notification wrapper and the
|
||||||
// rows). Anchor the dialog above that list.
|
// gym content wrapper; we insert before gymContentWrapper.
|
||||||
const candidates = [
|
const candidates = [
|
||||||
'ul[class*="properties"]',
|
'[class*="gymContentWrapper"]', // outer wrapper — best insertion point
|
||||||
'[class*="gymContent"]',
|
'[class*="gymContent"]', // inner wrapper (fallback)
|
||||||
'[class*="gymContentWrapper"]',
|
'ul[class*="properties"]', // the list itself (last resort)
|
||||||
// Legacy fallbacks (kept in case Torn ever wraps the list in a form):
|
// Legacy fallbacks (kept for robustness):
|
||||||
'form[action*="train"]',
|
'form[action*="train"]',
|
||||||
'form.train-form',
|
'form.train-form',
|
||||||
'form[class*="train"]',
|
'form[class*="train"]',
|
||||||
'[class*="train-button"]',
|
'[class*="train-button"]',
|
||||||
'button[class*="train"]',
|
'button[class*="train"]',
|
||||||
'a[class*="train"]',
|
'a[href*="train"]',
|
||||||
'button[name="train"]',
|
'button[name="train"]',
|
||||||
'a[href*="train"]',
|
'a[href*="train"]',
|
||||||
];
|
];
|
||||||
@@ -99,20 +99,13 @@ function start() {
|
|||||||
if (prefs.mode === 'anchored') {
|
if (prefs.mode === 'anchored') {
|
||||||
const el = findAnchorElement();
|
const el = findAnchorElement();
|
||||||
if (el) {
|
if (el) {
|
||||||
const rect = el.getBoundingClientRect();
|
dialog.setMode('anchored', { canAnchor: true, insertBefore: el });
|
||||||
dialog.setMode('anchored', { canAnchor: true });
|
|
||||||
dialog._positionAnchored(rect);
|
|
||||||
// observe
|
|
||||||
const ro = new ResizeObserver(() => {
|
|
||||||
if (prefs.mode === 'anchored') dialog._positionAnchored(el.getBoundingClientRect());
|
|
||||||
});
|
|
||||||
ro.observe(el);
|
|
||||||
anchorError = null;
|
anchorError = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Anchor selector missed — don't snap to default, just keep current
|
// Anchor selector missed — don't snap to default, just keep current
|
||||||
// position and show a note.
|
// position and show a note.
|
||||||
anchorError = "Couldn't find the training form on this page.";
|
anchorError = "Couldn't find the training UI on this page.";
|
||||||
render();
|
render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,17 @@ const STYLE = `
|
|||||||
font: 13px/1.4 Tahoma, Verdana, sans-serif;
|
font: 13px/1.4 Tahoma, Verdana, sans-serif;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
}
|
}
|
||||||
|
.tat-root.tat-anchored {
|
||||||
|
position: static;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
max-width: none;
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 0;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-top: 2px solid #c00;
|
||||||
|
padding: 16px 20px;
|
||||||
|
}
|
||||||
|
.tat-root.tat-anchored .tat-header { cursor: default; }
|
||||||
.tat-header {
|
.tat-header {
|
||||||
display: flex; justify-content: space-between; align-items: center;
|
display: flex; justify-content: space-between; align-items: center;
|
||||||
padding-bottom: 8px; margin-bottom: 10px;
|
padding-bottom: 8px; margin-bottom: 10px;
|
||||||
@@ -117,17 +128,37 @@ export class Dialog {
|
|||||||
setMode(mode, anchorInfo) {
|
setMode(mode, anchorInfo) {
|
||||||
this.mode = mode;
|
this.mode = mode;
|
||||||
if (!this.root) return;
|
if (!this.root) return;
|
||||||
|
// Clear all position styles
|
||||||
this.root.style.transform = '';
|
this.root.style.transform = '';
|
||||||
this.root.style.top = '';
|
this.root.style.top = '';
|
||||||
this.root.style.bottom = '';
|
this.root.style.bottom = '';
|
||||||
this.root.style.left = '';
|
this.root.style.left = '';
|
||||||
this.root.style.right = '';
|
this.root.style.right = '';
|
||||||
|
this.root.classList.remove('tat-anchored');
|
||||||
|
|
||||||
if (mode === 'free') {
|
if (mode === 'free') {
|
||||||
|
// Floating mode: ensure dialog is in body and position at bottom-left.
|
||||||
|
if (this.root.parentNode !== document.body) {
|
||||||
|
document.body.appendChild(this.root);
|
||||||
|
}
|
||||||
this.root.style.bottom = '20px';
|
this.root.style.bottom = '20px';
|
||||||
this.root.style.left = '20px';
|
this.root.style.left = '20px';
|
||||||
} else if (anchorInfo && anchorInfo.canAnchor) {
|
} else if (anchorInfo && anchorInfo.canAnchor) {
|
||||||
this._positionAnchored(anchorInfo.rect);
|
if (anchorInfo.insertBefore) {
|
||||||
|
// Docked mode: insert the dialog into the page flow before the
|
||||||
|
// given element, and add the tat-anchored class to switch to
|
||||||
|
// static positioning.
|
||||||
|
anchorInfo.insertBefore.parentNode.insertBefore(this.root, anchorInfo.insertBefore);
|
||||||
|
this.root.classList.add('tat-anchored');
|
||||||
|
} else if (anchorInfo.rect) {
|
||||||
|
// Fallback: position fixed above the rect (old behavior, used when
|
||||||
|
// no insertion point is available but a rect was given).
|
||||||
|
this._positionAnchored(anchorInfo.rect);
|
||||||
|
}
|
||||||
|
// If neither insertBefore nor rect, leave the dialog where it is
|
||||||
|
// (the caller will show an anchorError note).
|
||||||
} else {
|
} else {
|
||||||
|
// Top-center fallback (used when mode is anchored but no anchor info).
|
||||||
this.root.style.top = '20px';
|
this.root.style.top = '20px';
|
||||||
this.root.style.left = '50%';
|
this.root.style.left = '50%';
|
||||||
this.root.style.transform = 'translateX(-50%)';
|
this.root.style.transform = 'translateX(-50%)';
|
||||||
@@ -135,6 +166,7 @@ export class Dialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_positionAnchored(rect) {
|
_positionAnchored(rect) {
|
||||||
|
if (!rect) return; // defensive: setMode may be called without a rect
|
||||||
const dialogRect = this.root.getBoundingClientRect();
|
const dialogRect = this.root.getBoundingClientRect();
|
||||||
let top = rect.top - dialogRect.height - 8;
|
let top = rect.top - dialogRect.height - 8;
|
||||||
if (top < 8) top = 20;
|
if (top < 8) top = 20;
|
||||||
|
|||||||
@@ -131,7 +131,7 @@
|
|||||||
const current = tatExtractValueFromLi(li);
|
const current = tatExtractValueFromLi(li);
|
||||||
if (current == null) return null;
|
if (current == null) return null;
|
||||||
const gym = tatFindGymName() || 'Unknown gym';
|
const gym = tatFindGymName() || 'Unknown gym';
|
||||||
return { attr: attr, current: current, gym: tatEsc(gym) };
|
return { attr: attr, current: current, gym: gym };
|
||||||
}
|
}
|
||||||
function tatFindActiveAttributeLi() {
|
function tatFindActiveAttributeLi() {
|
||||||
// Priority 1: the <li> with the "success" class (just trained).
|
// Priority 1: the <li> with the "success" class (just trained).
|
||||||
@@ -276,6 +276,8 @@
|
|||||||
// ===== ui.js (embedded) =====
|
// ===== ui.js (embedded) =====
|
||||||
const TAT_STYLE = `
|
const TAT_STYLE = `
|
||||||
.tat-root { position: fixed; z-index: 99999; min-width: 320px; max-width: 420px; background: #2b2b2b; color: #ddd; border: 1px solid #444; border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.4); font: 13px/1.4 Tahoma, Verdana, sans-serif; padding: 12px 14px; }
|
.tat-root { position: fixed; z-index: 99999; min-width: 320px; max-width: 420px; background: #2b2b2b; color: #ddd; border: 1px solid #444; border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.4); font: 13px/1.4 Tahoma, Verdana, sans-serif; padding: 12px 14px; }
|
||||||
|
.tat-root.tat-anchored { position: static; margin: 0 0 12px 0; max-width: none; box-shadow: none; border-radius: 0; border: 1px solid #444; border-top: 2px solid #c00; padding: 16px 20px; }
|
||||||
|
.tat-root.tat-anchored .tat-header { cursor: default; }
|
||||||
.tat-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 8px; margin-bottom: 10px; border-bottom: 1px solid #444; cursor: move; user-select: none; }
|
.tat-header { display: flex; justify-content: space-between; align-items: center; padding-bottom: 8px; margin-bottom: 10px; border-bottom: 1px solid #444; cursor: move; user-select: none; }
|
||||||
.tat-header strong { color: #fff; }
|
.tat-header strong { color: #fff; }
|
||||||
.tat-close { cursor: pointer; opacity: 0.6; padding: 0 4px; }
|
.tat-close { cursor: pointer; opacity: 0.6; padding: 0 4px; }
|
||||||
@@ -350,12 +352,39 @@
|
|||||||
setMode(mode, anchorInfo) {
|
setMode(mode, anchorInfo) {
|
||||||
this.mode = mode;
|
this.mode = mode;
|
||||||
if (!this.root) return;
|
if (!this.root) return;
|
||||||
|
// Clear all position styles
|
||||||
this.root.style.transform = ''; this.root.style.top = ''; this.root.style.bottom = ''; this.root.style.left = ''; this.root.style.right = '';
|
this.root.style.transform = ''; this.root.style.top = ''; this.root.style.bottom = ''; this.root.style.left = ''; this.root.style.right = '';
|
||||||
if (mode === 'free') { this.root.style.bottom = '20px'; this.root.style.left = '20px'; }
|
this.root.classList.remove('tat-anchored');
|
||||||
else if (anchorInfo && anchorInfo.canAnchor) { this._positionAnchored(anchorInfo.rect); }
|
if (mode === 'free') {
|
||||||
else { this.root.style.top = '20px'; this.root.style.left = '50%'; this.root.style.transform = 'translateX(-50%)'; }
|
// Floating mode: ensure dialog is in body and position at bottom-left.
|
||||||
|
if (this.root.parentNode !== document.body) {
|
||||||
|
document.body.appendChild(this.root);
|
||||||
|
}
|
||||||
|
this.root.style.bottom = '20px';
|
||||||
|
this.root.style.left = '20px';
|
||||||
|
} else if (anchorInfo && anchorInfo.canAnchor) {
|
||||||
|
if (anchorInfo.insertBefore) {
|
||||||
|
// Docked mode: insert the dialog into the page flow before the
|
||||||
|
// given element, and add the tat-anchored class to switch to
|
||||||
|
// static positioning.
|
||||||
|
anchorInfo.insertBefore.parentNode.insertBefore(this.root, anchorInfo.insertBefore);
|
||||||
|
this.root.classList.add('tat-anchored');
|
||||||
|
} else if (anchorInfo.rect) {
|
||||||
|
// Fallback: position fixed above the rect (old behavior, used when
|
||||||
|
// no insertion point is available but a rect was given).
|
||||||
|
this._positionAnchored(anchorInfo.rect);
|
||||||
|
}
|
||||||
|
// If neither insertBefore nor rect, leave the dialog where it is
|
||||||
|
// (the caller will show an anchorError note).
|
||||||
|
} else {
|
||||||
|
// Top-center fallback (used when mode is anchored but no anchor info).
|
||||||
|
this.root.style.top = '20px';
|
||||||
|
this.root.style.left = '50%';
|
||||||
|
this.root.style.transform = 'translateX(-50%)';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_positionAnchored(rect) {
|
_positionAnchored(rect) {
|
||||||
|
if (!rect) return; // defensive: setMode may be called without a rect
|
||||||
const dialogRect = this.root.getBoundingClientRect();
|
const dialogRect = this.root.getBoundingClientRect();
|
||||||
let top = rect.top - dialogRect.height - 8;
|
let top = rect.top - dialogRect.height - 8;
|
||||||
if (top < 8) top = 20;
|
if (top < 8) top = 20;
|
||||||
@@ -438,20 +467,20 @@
|
|||||||
|
|
||||||
// ===== main.js (embedded) =====
|
// ===== main.js (embedded) =====
|
||||||
function findAnchorElement() {
|
function findAnchorElement() {
|
||||||
// Try several selectors in priority order. Torn's gym page renders the
|
// Return the element to insert the dialog BEFORE in the DOM.
|
||||||
// training UI as <ul class="properties___HASH"> (the list of attribute
|
// The user wants the dialog between the notification wrapper and the
|
||||||
// rows). Anchor the dialog above that list.
|
// gym content wrapper; we insert before gymContentWrapper.
|
||||||
const candidates = [
|
const candidates = [
|
||||||
'ul[class*="properties"]',
|
|
||||||
'[class*="gymContent"]',
|
|
||||||
'[class*="gymContentWrapper"]',
|
'[class*="gymContentWrapper"]',
|
||||||
|
'[class*="gymContent"]',
|
||||||
|
'ul[class*="properties"]',
|
||||||
// Legacy fallbacks (kept in case Torn ever wraps the list in a form):
|
// Legacy fallbacks (kept in case Torn ever wraps the list in a form):
|
||||||
'form[action*="train"]',
|
'form[action*="train"]',
|
||||||
'form.train-form',
|
'form.train-form',
|
||||||
'form[class*="train"]',
|
'form[class*="train"]',
|
||||||
'[class*="train-button"]',
|
'[class*="train-button"]',
|
||||||
'button[class*="train"]',
|
'button[class*="train"]',
|
||||||
'a[class*="train"]',
|
'a[href*="train"]',
|
||||||
'button[name="train"]',
|
'button[name="train"]',
|
||||||
'a[href*="train"]',
|
'a[href*="train"]',
|
||||||
];
|
];
|
||||||
@@ -515,21 +544,13 @@
|
|||||||
if (prefs.mode === 'anchored') {
|
if (prefs.mode === 'anchored') {
|
||||||
const el = findAnchorElement();
|
const el = findAnchorElement();
|
||||||
if (el) {
|
if (el) {
|
||||||
const rect = el.getBoundingClientRect();
|
dialog.setMode('anchored', { canAnchor: true, insertBefore: el });
|
||||||
dialog.setMode('anchored', { canAnchor: true });
|
|
||||||
dialog._positionAnchored(rect);
|
|
||||||
if (typeof ResizeObserver !== 'undefined') {
|
|
||||||
const ro = new ResizeObserver(function () {
|
|
||||||
if (prefs.mode === 'anchored') dialog._positionAnchored(el.getBoundingClientRect());
|
|
||||||
});
|
|
||||||
ro.observe(el);
|
|
||||||
}
|
|
||||||
anchorError = null;
|
anchorError = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Anchor selector missed — don't snap to default, just keep current
|
// Anchor selector missed — don't snap to default, just keep current
|
||||||
// position and show a note.
|
// position and show a note.
|
||||||
anchorError = "Couldn't find the training form on this page.";
|
anchorError = "Couldn't find the training UI on this page.";
|
||||||
render();
|
render();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user