179 lines
6.0 KiB
JavaScript
179 lines
6.0 KiB
JavaScript
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.width = `${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;
|
||
}
|
||
}
|
||
}
|
||
}
|