fix(ui): escape user strings, lazy drag listeners, drop dead warn field

This commit is contained in:
dev
2026-06-01 16:58:12 -05:00
parent 3053a6d713
commit 8502c53663
+34 -22
View File
@@ -65,6 +65,16 @@ function fmtDate(d) {
return d.toLocaleDateString('en-US', { weekday: 'short', day: '2-digit', month: 'short', year: 'numeric' }); return d.toLocaleDateString('en-US', { weekday: 'short', day: '2-digit', month: 'short', year: 'numeric' });
} }
function esc(s) {
if (s == null) return '';
return String(s)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
export class Dialog { export class Dialog {
constructor({ onTargetChange, onModeChange, onPosChange, onClose } = {}) { constructor({ onTargetChange, onModeChange, onPosChange, onClose } = {}) {
this.onTargetChange = onTargetChange; this.onTargetChange = onTargetChange;
@@ -74,7 +84,6 @@ export class Dialog {
this.root = null; this.root = null;
this.dragState = null; this.dragState = null;
this.mode = 'free'; this.mode = 'free';
this.warn = null;
} }
mount({ initialMode = 'free', initialPos = { x: 0, y: 0 } } = {}) { mount({ initialMode = 'free', initialPos = { x: 0, y: 0 } } = {}) {
@@ -136,31 +145,34 @@ export class Dialog {
} }
_wireHeaderDrag() { _wireHeaderDrag() {
const onDown = (e) => { const self = this;
if (this.mode !== 'free') return; this.root.addEventListener('mousedown', (e) => {
if (self.mode !== 'free') return;
if (e.target.classList.contains('tat-close')) return; if (e.target.classList.contains('tat-close')) return;
const rect = this.root.getBoundingClientRect(); const rect = self.root.getBoundingClientRect();
this.dragState = { dx: e.clientX - rect.left, dy: e.clientY - rect.top }; self.dragState = { dx: e.clientX - rect.left, dy: e.clientY - rect.top };
e.preventDefault(); e.preventDefault();
};
const onMove = (e) => { const onMove = (ev) => {
if (!this.dragState) return; if (!self.dragState) return;
const x = e.clientX - this.dragState.dx; const x = ev.clientX - self.dragState.dx;
const y = e.clientY - this.dragState.dy; const y = ev.clientY - self.dragState.dy;
this.root.style.left = `${x}px`; self.root.style.left = x + 'px';
this.root.style.top = `${y}px`; self.root.style.top = y + 'px';
this.root.style.right = 'auto'; self.root.style.right = 'auto';
this.root.style.bottom = 'auto'; self.root.style.bottom = 'auto';
}; };
const onUp = () => { const onUp = () => {
if (!this.dragState) return; if (!self.dragState) return;
const rect = this.root.getBoundingClientRect(); const r = self.root.getBoundingClientRect();
this.dragState = null; self.dragState = null;
this.onPosChange && this.onPosChange({ x: rect.left, y: rect.top }); self.onPosChange && self.onPosChange({ x: r.left, y: r.top });
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
}; };
this.root.addEventListener('mousedown', onDown);
document.addEventListener('mousemove', onMove); document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp); document.addEventListener('mouseup', onUp);
});
} }
render(state) { render(state) {
@@ -169,7 +181,7 @@ export class Dialog {
if (error) { if (error) {
this.root.innerHTML = ` this.root.innerHTML = `
<div class="tat-header"><strong>⚙ Attribute Tracker</strong><span class="tat-close">✕</span></div> <div class="tat-header"><strong>⚙ Attribute Tracker</strong><span class="tat-close">✕</span></div>
<div class="tat-error">${error}<button data-action="reload">Reload</button></div> <div class="tat-error">${esc(error)}<button data-action="reload">Reload</button></div>
`; `;
this.root.querySelector('[data-action="reload"]').onclick = () => location.reload(); this.root.querySelector('[data-action="reload"]').onclick = () => location.reload();
this.root.querySelector('.tat-close').onclick = () => this.onClose && this.onClose(); this.root.querySelector('.tat-close').onclick = () => this.onClose && this.onClose();
@@ -192,7 +204,7 @@ export class Dialog {
<strong>⚙ Attribute Tracker</strong> <strong>⚙ Attribute Tracker</strong>
<span class="tat-close" title="Hide for this session">✕</span> <span class="tat-close" title="Hide for this session">✕</span>
</div> </div>
<div class="tat-row"><span>Attribute</span><span><strong>${attr || '—'}</strong> · <em>${gym || '—'}</em></span></div> <div class="tat-row"><span>Attribute</span><span><strong>${esc(attr)}</strong> · <em>${esc(gym)}</em></span></div>
<div class="tat-row"><span>Current</span><span>${fmtFull(current)}</span></div> <div class="tat-row"><span>Current</span><span>${fmtFull(current)}</span></div>
<div class="tat-row tat-target"> <div class="tat-row tat-target">
<span>Target</span> <span>Target</span>
@@ -211,7 +223,7 @@ export class Dialog {
<div class="tat-row"><span>Trains to go</span><span>≈ ${fmtFull(est.trainsToGo)}</span></div> <div class="tat-row"><span>Trains to go</span><span>≈ ${fmtFull(est.trainsToGo)}</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-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">⚠ ${warn}</div>` : ''} ${warn ? `<div class="tat-warn">⚠ ${esc(warn)}</div>` : ''}
`; `;
this.root.querySelector('.tat-close').onclick = () => this.onClose && this.onClose(); this.root.querySelector('.tat-close').onclick = () => this.onClose && this.onClose();