Files
devplacepy/devplacepy/static/js/PollManager.js
T
retoor 34e33995c4
DevPlace CI / test (push) Successful in 32m28s
feat: add alt text extraction from URLs for images and improve CSS variable usage for progress bars
Add `_alt_from_url` helper in Python rendering and `altFromUrl` in JS ContentRenderer to generate descriptive alt attributes from image filenames, replacing empty alt strings. Update image embed templates and Avatar component to use derived alt text. Migrate inline `style.width` assignments to CSS custom properties (`--poll-pct`, `--hud-xp`, `--quest-pct`, `--bar-pct`, `--quota-pct`) across PollManager, GameFarm, and profile/quest CSS for better maintainability. Introduce new admin, docs, projects, services, and base CSS utility classes. Add "Code Farm" docs page entry.
2026-06-22 23:13:48 +00:00

181 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// retoor <retoor@molodetz.nl>
import { OptimisticAction } from "./OptimisticAction.js";
export class PollManager extends OptimisticAction {
constructor() {
super();
document.addEventListener("click", (event) => this.onClick(event));
document.addEventListener("submit", (event) => this.onSubmit(event), true);
}
onClick(event) {
const option = event.target.closest(".poll-option");
if (option && !option.disabled) {
event.preventDefault();
this.vote(option);
return;
}
const toggle = event.target.closest("[data-poll-toggle]");
if (toggle) {
event.preventDefault();
this.toggleBuilder(toggle);
return;
}
const addOption = event.target.closest("[data-poll-add-option]");
if (addOption) {
event.preventDefault();
this.addOption(addOption);
return;
}
const removeOption = event.target.closest("[data-poll-remove-option]");
if (removeOption) {
event.preventDefault();
this.removeOption(removeOption);
}
}
async vote(option) {
const poll = option.closest(".poll");
if (!poll) {
return;
}
const pollUid = poll.dataset.pollUid;
const optionUid = option.dataset.optionUid;
await this.submit(`/polls/${pollUid}/vote`, { option_uid: optionUid }, null, (result) => this.render(poll, result));
}
render(poll, result) {
const byUid = {};
(result.options || []).forEach((option) => {
byUid[option.uid] = option;
});
poll.querySelectorAll(".poll-option").forEach((button) => {
const data = byUid[button.dataset.optionUid];
if (!data) {
return;
}
const chosen = result.my_choice === data.uid;
button.classList.toggle("chosen", chosen);
button.setAttribute("aria-pressed", chosen ? "true" : "false");
const bar = button.querySelector(".poll-option-bar");
if (bar) {
bar.style.setProperty("--poll-pct", `${data.pct}%`);
}
const pct = button.querySelector(".poll-option-pct");
if (pct) {
pct.textContent = `${data.pct}%`;
}
const check = button.querySelector(".poll-option-check");
if (check) {
check.hidden = !chosen;
}
});
const total = poll.querySelector(".poll-total");
if (total) {
total.textContent = `${result.total} vote${result.total === 1 ? "" : "s"}`;
}
}
toggleBuilder(toggle) {
const form = toggle.closest("form");
const builder = form ? form.querySelector("[data-poll-builder]") : null;
if (!builder) {
return;
}
const activating = builder.hidden;
builder.hidden = !activating;
this.setPollEnabled(builder, activating);
const label = toggle.querySelector("[data-poll-toggle-label]");
if (label) {
label.textContent = activating ? "Remove poll" : "Add poll";
}
}
setPollEnabled(builder, enabled) {
builder.querySelectorAll("input[name='poll_question'], input[name='poll_options']").forEach((input) => {
input.disabled = !enabled;
if (!enabled) {
input.value = "";
}
});
const error = builder.querySelector("[data-poll-error]");
if (error) {
error.hidden = true;
error.textContent = "";
}
}
addOption(button) {
const form = button.closest("form");
const container = form ? form.querySelector("[data-poll-options]") : null;
if (!container) {
return;
}
const rows = container.querySelectorAll(".poll-builder-row");
if (rows.length >= 6) {
return;
}
const row = document.createElement("div");
row.className = "poll-builder-row";
const input = document.createElement("input");
input.type = "text";
input.name = "poll_options";
input.maxLength = 100;
input.placeholder = `Option ${rows.length + 1}`;
const remove = document.createElement("button");
remove.type = "button";
remove.className = "poll-builder-remove";
remove.dataset.pollRemoveOption = "";
remove.setAttribute("aria-label", "Remove option");
remove.textContent = "×";
row.appendChild(input);
row.appendChild(remove);
container.appendChild(row);
}
removeOption(button) {
const container = button.closest("[data-poll-options]");
if (!container) {
return;
}
if (container.querySelectorAll(".poll-builder-row").length <= 2) {
return;
}
const row = button.closest(".poll-builder-row");
if (row) {
row.remove();
}
}
onSubmit(event) {
const form = event.target;
if (!form || typeof form.querySelector !== "function") {
return;
}
const builder = form.querySelector("[data-poll-builder]");
if (!builder || builder.hidden) {
return;
}
const question = builder.querySelector("input[name='poll_question']");
const options = [...builder.querySelectorAll("input[name='poll_options']")]
.map((input) => input.value.trim())
.filter(Boolean);
let problem = "";
if (!question || !question.value.trim()) {
problem = "Add a poll question, or remove the poll.";
} else if (options.length < 2) {
problem = "Add at least two poll options, or remove the poll.";
}
if (problem) {
event.preventDefault();
event.stopImmediatePropagation();
const error = builder.querySelector("[data-poll-error]");
if (error) {
error.textContent = problem;
error.hidden = false;
}
}
}
}