Files
DarkflameServer/dDashboardServer/static/js/decide.js
Aaron Kimbrell f58255ab8a feat(dashboard): change pages without reloading the dashboard
nav.js (loaded on every signed-in page) follows links and GET forms to
other dashboard pages by fetching the page and swapping in its <main>,
title, page styles and page scripts; the sidebar, top bar and live
WebSocket stay. History entries, back/forward (with scroll positions,
also kept for a reload), deep links, #hash filters and breadcrumbs keep
working. What the old page set up is taken down first: its
document/window listeners, setInterval timers, Live watchers and
topics, DataTables, dialogs and what it appended to <body>. Page
scripts run again in order; DOMContentLoaded/load handlers they add run
once they have all run. A thin bar shows while loading; a failed fetch
shows an inline error with Try again / Open it normally.

Falls back to a normal load for anything that isn't a signed-in
dashboard page, when the account or permissions changed, and for pages
with module scripts or an import map (World 3D, property view and 3D,
UGC server) or data-nav="reload", and when leaving those. Unsaved-change
prompts (beforeunload) are asked before swapping.

Check: click around the sidebar and into accounts/characters: no white
flash, the footer's live indicator stays "live", back/forward return to
the same scroll position, breadcrumbs follow the path (Accounts > an
account > a character). Activity Log links with #search= still filter.
System Log's server picker works. Leaving Settings with a change asks
first. World 3D and a property's 3D view still load (normally). Pages
that poll (Server Health, Instance Load) stop polling once left
(browser network tab). Test: NavRulesJs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 02:12:13 -05:00

88 lines
4.3 KiB
JavaScript

/**
* The dialog for rejecting or removing something a player made (a name, pet name, property or leaderboard score): a reason,
* and for staff who may give strikes, a choice to also put a strike on the player's account. Not every rejection deserves
* one, so it starts unticked and shows how many strikes the account already has.
*
* Decide({ title, action, reasonLabel, reasonRequired, characterId }).then(function (choice) { choice.reason; choice.strike; })
*
* Resolves with null when cancelled.
*/
(function () {
'use strict';
var modal = null, el = null, pending = null;
function build() {
el = document.createElement('div');
el.className = 'modal fade';
el.dataset.navKeep = ''; // shared by every page: kept when nav.js swaps pages
el.tabIndex = -1;
el.setAttribute('aria-labelledby', 'decideTitle');
el.innerHTML =
'<div class="modal-dialog"><form class="modal-content" id="decideForm">' +
'<div class="modal-header"><h5 class="modal-title" id="decideTitle"></h5><button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button></div>' +
'<div class="modal-body">' +
'<p class="small text-body-secondary d-none" id="decideText"></p>' +
'<label class="form-label" for="decideReason" id="decideReasonLabel"></label>' +
'<textarea class="form-control" id="decideReason" rows="2" maxlength="300"></textarea>' +
'<div class="form-check mt-3 d-none" id="decideStrikeWrap">' +
'<input class="form-check-input" type="checkbox" id="decideStrike"><label class="form-check-label" for="decideStrike">Also give a strike on their account</label>' +
'<div class="form-text" id="decideStrikeInfo"></div></div>' +
'</div>' +
'<div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>' +
'<button type="submit" class="btn btn-danger" id="decideSubmit"></button></div>' +
'</form></div>';
document.body.appendChild(el);
modal = new bootstrap.Modal(el);
el.querySelector('#decideForm').addEventListener('submit', function (e) {
e.preventDefault();
var reason = el.querySelector('#decideReason');
if (reason.required && !reason.value.trim()) { reason.focus(); return; }
var done = pending;
pending = null;
modal.hide();
if (done) done({ reason: reason.value.trim(), strike: el.querySelector('#decideStrike').checked });
});
el.addEventListener('hidden.bs.modal', function () {
if (pending) { var done = pending; pending = null; done(null); }
});
el.addEventListener('shown.bs.modal', function () { el.querySelector('#decideReason').focus(); });
}
window.Decide = function (options) {
if (!el) build();
options = options || {};
// A prefill queued by another script (the AI helper's "Use this") for the next dialog: only filled in, never submitted
var prefill = window.Decide.next || {};
window.Decide.next = null;
el.querySelector('#decideTitle').textContent = options.title || 'Reject';
var text = el.querySelector('#decideText');
text.textContent = options.text || '';
text.classList.toggle('d-none', !options.text);
el.querySelector('#decideReasonLabel').textContent = options.reasonLabel || 'Reason';
var reason = el.querySelector('#decideReason');
reason.value = options.reason || prefill.reason || '';
reason.required = !!options.reasonRequired;
el.querySelector('#decideSubmit').textContent = options.action || 'Reject';
var strikeWrap = el.querySelector('#decideStrikeWrap'), strike = el.querySelector('#decideStrike'), info = el.querySelector('#decideStrikeInfo');
var canStrike = DASH.can('strikes_give') && options.characterId && options.characterId !== '0';
strike.checked = !!(canStrike && (options.strike || prefill.strike));
strikeWrap.classList.toggle('d-none', !canStrike);
info.textContent = '';
if (canStrike) {
api.get('/api/characters/' + options.characterId + '/strikes').then(function (d) {
if (!d.success) return;
info.innerHTML = 'Only if this deserves one. ' + esc(d.accountName || 'The account') + ' has ' +
(d.active ? '<strong>' + esc(d.active) + '</strong> active strike' + (d.active === 1 ? '' : 's') : 'no active strikes') +
(d.total > d.active ? ' (' + esc(d.total) + ' ever)' : '') + '.';
}).catch(function () {});
}
return new Promise(function (resolve) {
pending = resolve;
modal.show();
});
};
})();