Refactored polls module into layered architecture with typed domain objects, isolated SSE handling, and comprehensive tests.
CI / Formatting (push) Successful in 5s
CI / Linting (push) Successful in 4s
CI / Tests (Python 3.12) (push) Successful in 2m54s
CI / Tests (Python 3.13) (push) Successful in 2m54s
CI / Tests (Python 3.14) (push) Successful in 2m52s
CI / Type Checking (push) Successful in 9s
CI / Spelling (push) Successful in 5s

This commit is contained in:
2026-05-11 11:58:47 -04:00
parent 4918547c7d
commit 06b4a0bb9c
19 changed files with 4172 additions and 1002 deletions
+189 -79
View File
@@ -12,95 +12,205 @@
// See the License for the specific language governing permissions and
// limitations under the License.
(() => {
"use strict";
import { readJsonConfig } from "./shared/config.js";
import {
createTextElement,
requireElement,
setHidden,
} from "./shared/dom.js";
const config = window.CREATE_CONFIG;
if (!config) return;
class CreatePollForm {
constructor(root, config) {
this.root = root;
this.config = config;
this.form = requireElement(root, "#poll-form", HTMLFormElement);
this.container = requireElement(root, "#options-container", HTMLElement);
this.addButton = requireElement(root, "#add-option", HTMLButtonElement);
this.multiCheckbox = requireElement(
root,
"#allow_multiple",
HTMLInputElement
);
this.multiOptions = requireElement(
root,
"#multi-select-options",
HTMLElement
);
this.minSelectionsInput = requireElement(
root,
"#min_selections",
HTMLInputElement
);
this.maxSelectionsInput = requireElement(
root,
"#max_selections",
HTMLInputElement
);
}
const container = document.getElementById('options-container');
const addBtn = document.getElementById('add-option');
const multiCheck = document.getElementById('allow_multiple');
const multiOpts = document.getElementById('multi-select-options');
const form = document.getElementById('poll-form');
const maxOptions = config.maxOptions;
const maxOptionLength = config.maxOptionLength;
const minSel = document.getElementById('min_selections');
const maxSel = document.getElementById('max_selections');
start() {
this.ensureMinimumOptions();
this.renumberOptions();
this.setMultiSelectEnabled(this.multiCheckbox.checked);
this.bindEvents();
}
const syncSelectionLimits = () => {
const minVal = parseInt(minSel.value, 10);
const maxVal = parseInt(maxSel.value, 10);
maxSel.min = minVal >= 1 ? minVal : 1;
minSel.max = maxVal >= 1 ? maxVal : minSel.max;
};
bindEvents() {
this.addButton.addEventListener("click", () => this.addOption());
this.container.addEventListener("click", (event) =>
this.removeOption(event)
);
this.multiCheckbox.addEventListener("change", () =>
this.setMultiSelectEnabled(this.multiCheckbox.checked)
);
this.minSelectionsInput.addEventListener("input", () =>
this.updateSelectionConstraints()
);
this.maxSelectionsInput.addEventListener("input", () =>
this.updateSelectionConstraints()
);
this.form.addEventListener("submit", () =>
this.updateSelectionConstraints()
);
}
const updateSelectionDefaults = () => {
const count = container.querySelectorAll('.option-row').length;
minSel.max = count;
maxSel.max = count;
maxSel.placeholder = count;
if (minSel.value && parseInt(minSel.value, 10) > count) {
minSel.value = count;
optionRows() {
return Array.from(this.container.querySelectorAll(".option-row"));
}
ensureMinimumOptions() {
while (this.optionRows().length < this.config.minOptions) {
this.container.appendChild(this.buildOptionRow());
}
if (maxSel.value && parseInt(maxSel.value, 10) > count) {
maxSel.value = count;
}
addOption() {
if (this.optionRows().length >= this.config.maxOptions) {
return;
}
syncSelectionLimits();
};
this.container.appendChild(this.buildOptionRow());
this.renumberOptions();
}
minSel.addEventListener('input', syncSelectionLimits);
maxSel.addEventListener('input', syncSelectionLimits);
const renumber = () => {
const rows = container.querySelectorAll('.option-row');
for (const [i, row] of [...rows].entries()) {
const num = i + 1;
row.querySelector('.option-number').textContent = num;
const input = row.querySelector('.option-input');
input.name = `option_${num}`;
input.placeholder = `Option ${num}`;
removeOption(event) {
const target = event.target;
if (!(target instanceof Element)) {
return;
}
addBtn.disabled = rows.length >= maxOptions;
updateSelectionDefaults();
};
addBtn.addEventListener('click', () => {
const rows = container.querySelectorAll('.option-row');
if (rows.length >= maxOptions) return;
const num = rows.length + 1;
const div = document.createElement('div');
div.className = 'input-group mb-2 option-row';
div.innerHTML =
`<span class="input-group-text option-number">${num}</span>` +
`<input type="text" class="form-control option-input" name="option_${num}" required maxlength="${maxOptionLength}" placeholder="Option ${num}">` +
'<button type="button" class="btn btn-outline-danger remove-option">Remove</button>';
container.appendChild(div);
renumber();
});
container.addEventListener('click', (e) => {
if (e.target.classList.contains('remove-option')) {
e.target.closest('.option-row').remove();
renumber();
const button = target.closest(".remove-option");
if (button === null || !this.container.contains(button)) {
return;
}
});
multiCheck.addEventListener('change', () => {
if (multiCheck.checked) {
updateSelectionDefaults();
multiOpts.classList.remove('d-none');
} else {
multiOpts.classList.add('d-none');
minSel.value = '';
maxSel.value = '';
const row = button.closest(".option-row");
if (row === null || this.optionRows().length <= this.config.minOptions) {
return;
}
});
form.addEventListener('submit', () => {
const lines = [...container.querySelectorAll('.option-input')]
.map(input => input.value.trim())
.filter(Boolean);
document.getElementById('options-hidden').value = lines.join('\n');
});
})();
row.remove();
this.renumberOptions();
}
buildOptionRow() {
const row = document.createElement("div");
row.className = "input-group mb-2 option-row";
const number = this.optionRows().length + 1;
const numberLabel = createTextElement(
"span",
"input-group-text option-number",
String(number)
);
const input = document.createElement("input");
input.type = "text";
input.className = "form-control option-input";
input.name = "options";
input.required = true;
input.maxLength = this.config.maxOptionLength;
input.placeholder = `Option ${number}`;
row.append(numberLabel, input);
if (number > this.config.minOptions) {
const removeButton = createTextElement(
"button",
"btn btn-outline-danger remove-option",
"Remove"
);
removeButton.type = "button";
row.appendChild(removeButton);
}
return row;
}
renumberOptions() {
const rows = this.optionRows();
for (const [index, row] of rows.entries()) {
const number = index + 1;
requireElement(row, ".option-number", HTMLElement).textContent =
String(number);
requireElement(row, ".option-input", HTMLInputElement).placeholder =
`Option ${number}`;
}
this.addButton.disabled = rows.length >= this.config.maxOptions;
this.updateSelectionConstraints();
}
setMultiSelectEnabled(enabled) {
this.minSelectionsInput.disabled = !enabled;
this.maxSelectionsInput.disabled = !enabled;
setHidden(this.multiOptions, !enabled);
if (!enabled) {
this.minSelectionsInput.value = "";
this.maxSelectionsInput.value = "";
this.minSelectionsInput.setCustomValidity("");
this.maxSelectionsInput.setCustomValidity("");
return;
}
this.updateSelectionConstraints();
}
updateSelectionConstraints() {
const optionCount = this.optionRows().length;
const minValue = this.parseSelectionValue(this.minSelectionsInput);
const maxValue = this.parseSelectionValue(this.maxSelectionsInput);
this.minSelectionsInput.max = String(optionCount);
this.maxSelectionsInput.max = String(optionCount);
this.maxSelectionsInput.placeholder = String(optionCount);
this.maxSelectionsInput.min =
Number.isInteger(minValue) && minValue >= 1 ? String(minValue) : "1";
this.minSelectionsInput.setCustomValidity("");
this.maxSelectionsInput.setCustomValidity("");
if (
!this.multiCheckbox.checked ||
!Number.isInteger(minValue) ||
!Number.isInteger(maxValue)
) {
return;
}
if (minValue > maxValue) {
this.maxSelectionsInput.setCustomValidity(
"Maximum selections must be at least the minimum."
);
}
}
parseSelectionValue(input) {
if (input.value.trim() === "") {
return null;
}
return Number(input.value);
}
}
const config = readJsonConfig("create-config");
new CreatePollForm(document, config).start();