added type='button' to both guard portal btns so firefox won't normalize to type='submit'; fixed several FTs for new click-guard functionality on Role card select & room gear menu DEL & BYE btns; several restorations to landscape breakpoint incl. logged-ion display_name, copyright info; provided title to room_scroll.html; a slurry of other minor fixes
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
This commit is contained in:
@@ -104,11 +104,26 @@ var RoleSelect = (function () {
|
||||
card.classList.add("flipped");
|
||||
});
|
||||
card.addEventListener("mouseleave", function () {
|
||||
card.classList.remove("flipped");
|
||||
if (!card.classList.contains("guard-active")) {
|
||||
card.classList.remove("flipped");
|
||||
}
|
||||
});
|
||||
card.addEventListener("click", function (e) {
|
||||
e.stopPropagation();
|
||||
selectRole(role.code, card);
|
||||
card.classList.add("flipped");
|
||||
card.classList.add("guard-active");
|
||||
window.showGuard(
|
||||
card,
|
||||
"Start round 1 as<br>" + role.name + " (" + role.code + ") …?",
|
||||
function () { // confirm
|
||||
card.classList.remove("guard-active");
|
||||
selectRole(role.code, card);
|
||||
},
|
||||
function () { // dismiss (NVM / outside click)
|
||||
card.classList.remove("guard-active");
|
||||
card.classList.remove("flipped");
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
modal.appendChild(card);
|
||||
|
||||
@@ -118,6 +118,13 @@ class FunctionalTest(StaticLiveServerTestCase):
|
||||
)
|
||||
)
|
||||
|
||||
def confirm_guard(self, browser=None):
|
||||
b = browser or self.browser
|
||||
def _click():
|
||||
btn = b.find_element(By.CSS_SELECTOR, "#id_guard_portal.active .guard-yes")
|
||||
b.execute_script("arguments[0].click()", btn)
|
||||
self.wait_for(_click)
|
||||
|
||||
@wait
|
||||
def wait_to_be_logged_in(self, email):
|
||||
self.browser.find_element(By.CSS_SELECTOR, "#id_logout"),
|
||||
@@ -199,6 +206,13 @@ class ChannelsFunctionalTest(ChannelsLiveServerTestCase):
|
||||
raise e
|
||||
time.sleep(0.5)
|
||||
|
||||
def confirm_guard(self, browser=None):
|
||||
b = browser or self.browser
|
||||
def _click():
|
||||
btn = b.find_element(By.CSS_SELECTOR, "#id_guard_portal.active .guard-yes")
|
||||
b.execute_script("arguments[0].click()", btn)
|
||||
self.wait_for(_click)
|
||||
|
||||
def create_pre_authenticated_session(self, email):
|
||||
if self.test_server:
|
||||
session_key = create_session_on_server(self.test_server, email)
|
||||
|
||||
@@ -209,6 +209,7 @@ class GatekeeperTest(FunctionalTest):
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-danger")
|
||||
).click()
|
||||
self.confirm_guard()
|
||||
|
||||
self.wait_for(lambda: self.assertEqual(
|
||||
self.browser.current_url, self.live_server_url + "/gameboard/"
|
||||
@@ -248,6 +249,7 @@ class GatekeeperTest(FunctionalTest):
|
||||
self.wait_for(
|
||||
lambda: self.browser.find_element(By.CSS_SELECTOR, ".btn-abandon")
|
||||
).click()
|
||||
self.confirm_guard()
|
||||
|
||||
self.wait_for(lambda: self.assertEqual(
|
||||
self.browser.current_url, self.live_server_url + "/gameboard/"
|
||||
|
||||
@@ -57,5 +57,6 @@ class LoginTest(FunctionalTest):
|
||||
self.wait_to_be_logged_in(email=TEST_EMAIL)
|
||||
|
||||
self.browser.find_element(By.CSS_SELECTOR, "#id_logout").click()
|
||||
self.confirm_guard()
|
||||
|
||||
self.wait_to_be_logged_out(email=TEST_EMAIL)
|
||||
|
||||
@@ -206,6 +206,7 @@ class RoleSelectTest(FunctionalTest):
|
||||
|
||||
# 5. Click first card to select it
|
||||
cards[0].click()
|
||||
self.confirm_guard()
|
||||
|
||||
# 6. Modal closes
|
||||
self.wait_for(
|
||||
@@ -436,6 +437,7 @@ class RoleSelectTest(FunctionalTest):
|
||||
).click()
|
||||
self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select"))
|
||||
self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
|
||||
self.confirm_guard()
|
||||
|
||||
# No WS — only the JS fix can make this transition happen
|
||||
self.wait_for(
|
||||
@@ -475,6 +477,7 @@ class RoleSelectTest(FunctionalTest):
|
||||
).click()
|
||||
self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select"))
|
||||
self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
|
||||
self.confirm_guard()
|
||||
|
||||
# Wait for fan to close (selectRole closes it synchronously)
|
||||
self.wait_for(
|
||||
@@ -588,6 +591,7 @@ class RoleSelectChannelsTest(ChannelsFunctionalTest):
|
||||
self.browser2.find_element(By.CSS_SELECTOR, ".card-stack").click()
|
||||
self.wait_for(lambda: self.browser2.find_element(By.ID, "id_role_select"))
|
||||
self.browser2.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
|
||||
self.confirm_guard(browser=self.browser2)
|
||||
|
||||
# 3. Watcher's seat arc moves to slot 2 — no page refresh
|
||||
self.wait_for(lambda: self.browser.find_element(
|
||||
@@ -656,6 +660,7 @@ class RoleSelectChannelsTest(ChannelsFunctionalTest):
|
||||
self.browser.find_element(By.CSS_SELECTOR, ".card-stack").click()
|
||||
self.wait_for(lambda: self.browser.find_element(By.ID, "id_role_select"))
|
||||
self.browser.find_element(By.CSS_SELECTOR, "#id_role_select .card").click()
|
||||
self.confirm_guard()
|
||||
|
||||
# 4. Friend's stack becomes eligible via WebSocket — no page refresh
|
||||
self.wait_for(lambda: self.browser2.find_element(
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
}
|
||||
|
||||
// In landscape: shift gear btn and applet menus left of the footer right sidebar
|
||||
@media (orientation: landscape) and (max-width: 1023px) {
|
||||
@media (orientation: landscape) and (max-width: 1440px) {
|
||||
$sidebar-w: 4rem;
|
||||
|
||||
.gameboard-page,
|
||||
|
||||
@@ -173,8 +173,8 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
@media (orientation: landscape) and (max-width: 1023px) {
|
||||
$sidebar-w: 4rem;
|
||||
@media (orientation: landscape) and (max-width: 1440px) {
|
||||
$sidebar-w: 5rem;
|
||||
|
||||
// ── Sidebar layout: navbar ← left, footer → right ────────────────────────────
|
||||
body {
|
||||
@@ -198,34 +198,49 @@ body {
|
||||
.container-fluid {
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
max-height: 700px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
gap: 1rem;
|
||||
padding: 0 0.25rem;
|
||||
|
||||
> form { margin-left: 0; flex-shrink: 0; order: -1; } // logout at top
|
||||
> form { flex-shrink: 0; order: -1; } // logout above brand
|
||||
}
|
||||
|
||||
.navbar-brand { order: 1; } // brand at bottom
|
||||
|
||||
.navbar-brand h1 {
|
||||
writing-mode: vertical-rl;
|
||||
transform: rotate(180deg);
|
||||
font-size: 1rem;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
margin-right: 2.75rem;
|
||||
// margin-right: 3.25rem;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
order: 1; // brand at bottom
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.navbar-text,
|
||||
.navbar-link { display: none; }
|
||||
|
||||
.navbar-text {
|
||||
writing-mode: vertical-rl;
|
||||
transform: rotate(180deg);
|
||||
font-size: 0.65rem;
|
||||
white-space: nowrap;
|
||||
margin: auto 0;
|
||||
|
||||
.navbar-label { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
font-size: 0.75rem;
|
||||
border-width: 0.125rem;
|
||||
margin-left: 0.75rem;
|
||||
// margin-left: 0.75rem;
|
||||
}
|
||||
|
||||
// Login form: email input can't fit in narrow sidebar
|
||||
@@ -275,7 +290,7 @@ body {
|
||||
flex-direction: column;
|
||||
width: auto;
|
||||
max-width: none;
|
||||
gap: 1.5rem;
|
||||
gap: 3rem;
|
||||
|
||||
a {
|
||||
font-size: 1.75rem;
|
||||
@@ -285,7 +300,16 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
.footer-container { display: none; }
|
||||
.footer-container {
|
||||
position: absolute;
|
||||
bottom: 0.75rem;
|
||||
text-align: center;
|
||||
font-size: 0.55rem;
|
||||
line-height: 1.4;
|
||||
color: rgba(var(--secUser), 0.5);
|
||||
|
||||
br { display: block; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -293,15 +317,16 @@ body {
|
||||
body .container {
|
||||
.navbar {
|
||||
padding: 0 0 0.25rem 0;
|
||||
|
||||
.navbar-brand h1 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
font-size: 0.75rem;
|
||||
border-width: 0.125rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
font-size: 0.75rem;
|
||||
border-width: 0.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,25 +344,25 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) and (max-height: 700px) {
|
||||
body .container .navbar {
|
||||
padding: 0.5rem 0;
|
||||
// @media (min-width: 1024px) and (max-height: 700px) {
|
||||
// body .container .navbar {
|
||||
// padding: 0.5rem 0;
|
||||
|
||||
.navbar-brand h1 {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
}
|
||||
// .navbar-brand h1 {
|
||||
// font-size: 1.4rem;
|
||||
// }
|
||||
// }
|
||||
|
||||
#id_footer {
|
||||
height: 3.5rem;
|
||||
padding: 0.7rem 1rem;
|
||||
gap: 0.35rem;
|
||||
// #id_footer {
|
||||
// height: 3.5rem;
|
||||
// padding: 0.7rem 1rem;
|
||||
// gap: 0.35rem;
|
||||
|
||||
#id_footer_nav a {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
// #id_footer_nav a {
|
||||
// font-size: 1.2rem;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
#id_footer {
|
||||
flex-shrink: 0;
|
||||
@@ -386,9 +411,41 @@ body {
|
||||
}
|
||||
|
||||
.footer-container {
|
||||
br { display: none; }
|
||||
|
||||
small {
|
||||
font-size: 0.7rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#id_guard_portal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 10000;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
background-color: rgba(var(--tooltip-bg), 0.5);
|
||||
backdrop-filter: blur(6px);
|
||||
border: 0.1rem solid rgba(var(--secUser), 0.4);
|
||||
box-shadow: 0 0.25rem 1rem rgba(0, 0, 0, 0.4);
|
||||
|
||||
&.active {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.guard-message {
|
||||
font-size: 0.85rem;
|
||||
color: rgba(var(--secUser), 0.9);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.guard-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
bottom: 0.5rem;
|
||||
right: 0.5rem;
|
||||
|
||||
@media (orientation: landscape) and (max-width: 1023px) {
|
||||
@media (orientation: landscape) and (max-width: 1440px) {
|
||||
right: calc(4rem + 0.5rem);
|
||||
bottom: 0.75rem;
|
||||
top: auto;
|
||||
|
||||
@@ -13,11 +13,15 @@ describe("RoleSelect", () => {
|
||||
window.fetch = jasmine.createSpy("fetch").and.returnValue(
|
||||
Promise.resolve({ ok: true })
|
||||
);
|
||||
// Default stub: auto-confirm so existing card-click tests pass unchanged.
|
||||
// The click-guard integration describe overrides this with a capturing spy.
|
||||
window.showGuard = (_anchor, _msg, onConfirm) => onConfirm && onConfirm();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
RoleSelect.closeFan();
|
||||
testDiv.remove();
|
||||
delete window.showGuard;
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
@@ -237,4 +241,145 @@ describe("RoleSelect", () => {
|
||||
expect(document.querySelector(".role-select-backdrop")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
// ------------------------------------------------------------------ //
|
||||
// click-guard integration //
|
||||
// ------------------------------------------------------------------ //
|
||||
// NOTE: cascade prevention (outside-click on backdrop not closing the //
|
||||
// fan while the guard is active) relies on the guard portal's capture- //
|
||||
// phase stopPropagation, which lives in base.html and requires //
|
||||
// integration testing. The callback contract is fully covered below. //
|
||||
// ------------------------------------------------------------------ //
|
||||
|
||||
describe("click-guard integration", () => {
|
||||
let guardAnchor, guardMessage, guardConfirm, guardDismiss;
|
||||
|
||||
beforeEach(() => {
|
||||
window.showGuard = jasmine.createSpy("showGuard").and.callFake(
|
||||
(anchor, message, onConfirm, onDismiss) => {
|
||||
guardAnchor = anchor;
|
||||
guardMessage = message;
|
||||
guardConfirm = onConfirm;
|
||||
guardDismiss = onDismiss;
|
||||
}
|
||||
);
|
||||
RoleSelect.openFan();
|
||||
});
|
||||
|
||||
describe("clicking a card", () => {
|
||||
let card;
|
||||
|
||||
beforeEach(() => {
|
||||
card = document.querySelector("#id_role_select .card");
|
||||
card.click();
|
||||
});
|
||||
|
||||
it("calls window.showGuard", () => {
|
||||
expect(window.showGuard).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes the card element as the anchor", () => {
|
||||
expect(guardAnchor).toBe(card);
|
||||
});
|
||||
|
||||
it("message contains the role name", () => {
|
||||
const roleName = card.querySelector(".card-role-name").textContent.trim();
|
||||
expect(guardMessage).toContain(roleName);
|
||||
});
|
||||
|
||||
it("message contains the role code", () => {
|
||||
expect(guardMessage).toContain(card.dataset.role);
|
||||
});
|
||||
|
||||
it("message contains a <br>", () => {
|
||||
expect(guardMessage).toContain("<br>");
|
||||
});
|
||||
|
||||
it("does not immediately close the fan", () => {
|
||||
expect(document.querySelector(".role-select-backdrop")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("does not immediately POST to the select_role URL", () => {
|
||||
expect(window.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("adds .flipped to the card", () => {
|
||||
expect(card.classList.contains("flipped")).toBe(true);
|
||||
});
|
||||
|
||||
it("adds .guard-active to the card", () => {
|
||||
expect(card.classList.contains("guard-active")).toBe(true);
|
||||
});
|
||||
|
||||
it("mouseleave does not remove .flipped while guard is active", () => {
|
||||
card.dispatchEvent(new MouseEvent("mouseleave"));
|
||||
expect(card.classList.contains("flipped")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("confirming the guard (OK)", () => {
|
||||
let card;
|
||||
|
||||
beforeEach(() => {
|
||||
card = document.querySelector("#id_role_select .card");
|
||||
card.click();
|
||||
guardConfirm();
|
||||
});
|
||||
|
||||
it("removes .guard-active from the card", () => {
|
||||
expect(card.classList.contains("guard-active")).toBe(false);
|
||||
});
|
||||
|
||||
it("closes the fan", () => {
|
||||
expect(document.querySelector(".role-select-backdrop")).toBeNull();
|
||||
});
|
||||
|
||||
it("POSTs to the select_role URL", () => {
|
||||
expect(window.fetch).toHaveBeenCalledWith(
|
||||
"/epic/room/test-uuid/select-role",
|
||||
jasmine.objectContaining({ method: "POST" })
|
||||
);
|
||||
});
|
||||
|
||||
it("appends a .card to #id_inv_role_card", () => {
|
||||
expect(document.querySelector("#id_inv_role_card .card")).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("dismissing the guard (NVM or outside click)", () => {
|
||||
let card;
|
||||
|
||||
beforeEach(() => {
|
||||
card = document.querySelector("#id_role_select .card");
|
||||
card.click();
|
||||
guardDismiss();
|
||||
});
|
||||
|
||||
it("removes .guard-active from the card", () => {
|
||||
expect(card.classList.contains("guard-active")).toBe(false);
|
||||
});
|
||||
|
||||
it("removes .flipped from the card", () => {
|
||||
expect(card.classList.contains("flipped")).toBe(false);
|
||||
});
|
||||
|
||||
it("leaves the fan open", () => {
|
||||
expect(document.querySelector(".role-select-backdrop")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("does not POST to the select_role URL", () => {
|
||||
expect(window.fetch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not add a card to inventory", () => {
|
||||
expect(document.querySelector("#id_inv_role_card .card")).toBeNull();
|
||||
});
|
||||
|
||||
it("restores normal mouseleave behaviour on the card", () => {
|
||||
card.dispatchEvent(new MouseEvent("mouseenter"));
|
||||
card.dispatchEvent(new MouseEvent("mouseleave"));
|
||||
expect(card.classList.contains("flipped")).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends "core/base.html" %}
|
||||
|
||||
{% block title_text %}{{ room.name }} — Drama Log{% endblock %}
|
||||
{% block title_text %}{{ room.name }} — Billscroll{% endblock %}
|
||||
{% block header_text %}<span>Bill</span>scroll{% endblock header_text %}
|
||||
|
||||
{% block content %}
|
||||
<div class="billboard-page">
|
||||
|
||||
@@ -1,27 +1,4 @@
|
||||
{% include "apps/gameboard/_partials/_applet_menu.html" %}
|
||||
<div id="id_game_applets_container">
|
||||
<div id="id_game_applet_menu" style="display:none;">
|
||||
<form
|
||||
hx-post="{% url "toggle_game_applets" %}"
|
||||
hx-target="#id_game_applets_container"
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
{% csrf_token %}
|
||||
{% for entry in applets %}
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
name="applets"
|
||||
value="{{ entry.applet.slug }}"
|
||||
{% if entry.visible %}checked{% endif %}
|
||||
>
|
||||
{{ entry.applet.name }}
|
||||
</label>
|
||||
{% endfor %}
|
||||
<div class="menu-btns">
|
||||
<button type="submit" class="btn btn-confirm">OK</button>
|
||||
<button type="button" id="id_game_applet_menu_cancel" class="btn btn-cancel applet-menu-cancel">NVM</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% include "apps/applets/_partials/_applets.html" %}
|
||||
</div>
|
||||
@@ -3,12 +3,12 @@
|
||||
{% if request.user == room.owner %}
|
||||
<form method="POST" action="{% url 'epic:delete_room' room.id %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger">DEL</button>
|
||||
<button type="submit" class="btn btn-danger" data-confirm="Delete this room?">DEL</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form method="POST" action="{% url 'epic:abandon_room' room.id %}">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-abandon">BYE</button>
|
||||
<button type="submit" class="btn btn-abandon" data-confirm="Leave this room?">BYE</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,6 @@
|
||||
</a>
|
||||
</nav>
|
||||
<div class="footer-container">
|
||||
<small>©{% now "Y" %} Dis Co.</small>
|
||||
<small>©{% now "Y" %} <br>Dis Co.</small>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</div>
|
||||
<form method="POST" action="{% url "logout" %}">
|
||||
{% csrf_token %}
|
||||
<button id="id_logout" class="btn btn-primary btn-xl" type="submit">
|
||||
<button id="id_logout" class="btn btn-primary btn-xl" type="submit" data-confirm="Log out?">
|
||||
Log Out
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -57,8 +57,91 @@
|
||||
{% endif %}
|
||||
<dialog id="id_kit_bag_dialog"></dialog>
|
||||
|
||||
<div id="id_guard_portal">
|
||||
<span class="guard-message"></span>
|
||||
<div class="guard-actions">
|
||||
<button class="btn btn-confirm guard-yes" type="button">OK</button>
|
||||
<button class="btn btn-cancel guard-no" type="button">NVM</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% block scripts %}
|
||||
{% endblock scripts %}
|
||||
<script>
|
||||
(function () {
|
||||
var portal = null;
|
||||
var _cb = null;
|
||||
var _onDismiss = null;
|
||||
|
||||
function show(anchor, message, callback, onDismiss) {
|
||||
if (!portal) return;
|
||||
_cb = callback;
|
||||
_onDismiss = onDismiss || null;
|
||||
portal.querySelector('.guard-message').innerHTML = message;
|
||||
portal.classList.add('active');
|
||||
var rect = anchor.getBoundingClientRect();
|
||||
var pw = portal.offsetWidth;
|
||||
var rawLeft = rect.left + rect.width / 2;
|
||||
var cleft = Math.max(pw / 2 + 8, Math.min(rawLeft, window.innerWidth - pw / 2 - 8));
|
||||
portal.style.left = Math.round(cleft) + 'px';
|
||||
if (rect.top > 120) {
|
||||
portal.style.top = Math.round(rect.top) + 'px';
|
||||
portal.style.transform = 'translate(-50%, calc(-100% - 0.5rem))';
|
||||
} else {
|
||||
portal.style.top = Math.round(rect.bottom) + 'px';
|
||||
portal.style.transform = 'translate(-50%, 0.5rem)';
|
||||
}
|
||||
}
|
||||
|
||||
function dismiss() {
|
||||
if (!portal) return;
|
||||
var od = _onDismiss;
|
||||
portal.classList.remove('active');
|
||||
_cb = null;
|
||||
_onDismiss = null;
|
||||
if (od) od();
|
||||
}
|
||||
|
||||
function doConfirm() {
|
||||
var cb = _cb;
|
||||
portal.classList.remove('active');
|
||||
_cb = null;
|
||||
_onDismiss = null;
|
||||
if (cb) cb();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
portal = document.getElementById('id_guard_portal');
|
||||
if (!portal) return;
|
||||
portal.querySelector('.guard-yes').addEventListener('click', doConfirm);
|
||||
portal.querySelector('.guard-no').addEventListener('click', dismiss);
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape') dismiss();
|
||||
});
|
||||
// Outside-click to dismiss — capture phase + stopPropagation
|
||||
// prevents the click from cascading to backdrop listeners (e.g. closeFan)
|
||||
document.addEventListener('click', function (e) {
|
||||
if (!portal.classList.contains('active')) return;
|
||||
if (portal.contains(e.target)) return;
|
||||
e.stopPropagation();
|
||||
dismiss();
|
||||
}, true);
|
||||
// Intercept [data-confirm] buttons (capture phase, before form submits)
|
||||
document.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest('[data-confirm]');
|
||||
if (!btn) return;
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
var form = btn.closest('form');
|
||||
show(btn, btn.dataset.confirm, function () {
|
||||
if (form) form.submit();
|
||||
});
|
||||
}, true);
|
||||
});
|
||||
|
||||
window.showGuard = show;
|
||||
}());
|
||||
</script>
|
||||
<script src="{% static "vendor/htmx.min.js" %}"></script>
|
||||
<script src="{% static "apps/applets/applets.js" %}"></script>
|
||||
<script src="{% static "apps/dashboard/game-kit.js" %}"></script>
|
||||
|
||||
Reference in New Issue
Block a user