Implement Form Expansion

If more than 3 options are written down, additional inputs will load in
to allow more options. The maximum cap of options currently is set to
255 but can be configured in Config.ts.
Likewise, the input length can also be controlled from there.
This commit is contained in:
Wolvan 2022-01-01 14:36:25 +01:00
parent 678342a9c6
commit c00ea29b4f
6 changed files with 59 additions and 8 deletions

View file

@ -4,5 +4,9 @@
"commonjs": false, "commonjs": false,
"es6": true, "es6": true,
"node": false "node": false
},
"globals": {
"MAX_POLL_OPTIONS": "readonly",
"MAX_CHARACTER_LENGTH": "readonly"
} }
} }

View file

@ -8,6 +8,11 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Caveat:wght@400;500;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Caveat:wght@400;500;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/main.css"> <link rel="stylesheet" href="/static/css/main.css">
<script type="text/javascript">
const MAX_POLL_OPTIONS = "{{ MAX_POLL_OPTIONS }}";
const MAX_CHARACTER_LENGTH = "{{ MAX_CHARACTER_LENGTH }}";
</script>
<script type="text/javascript" src="/static/js/index.js" defer="true" async="true"></script>
</head> </head>
<body> <body>
<header> <header>
@ -22,7 +27,7 @@
<div class="notepad-border"></div> <div class="notepad-border"></div>
<form action="{{ BACKEND_BASE_PATH }}/_backend/poll-form" method="POST"> <form action="{{ BACKEND_BASE_PATH }}/_backend/poll-form" method="POST">
<section class="poll-title"> <section class="poll-title">
<input id="poll-title" name="poll-title" type="text" maxlength="300" placeholder="Enter your question here" value="{{ FORM_TITLE }}"> <input id="poll-title" name="poll-title" type="text" maxlength="{{ MAX_CHARACTER_LENGTH }}" placeholder="Enter your question here" value="{{ FORM_TITLE }}">
</section> </section>
<section id="poll-options" class="poll-options"> <section id="poll-options" class="poll-options">
{{ FORM_OPTION_DIVS }} {{ FORM_OPTION_DIVS }}

View file

@ -0,0 +1,32 @@
"use strict";
(() => {
const inputList = [];
const pollOptionsAnchor = document.querySelector(".poll-options");
function createPollOptionInput() {
if (inputList.length >= parseInt(MAX_POLL_OPTIONS)) return;
const optionEl = document.createElement("div");
optionEl.classList.add("poll-option");
const input = document.createElement("input");
input.type = "text";
input.maxLength = MAX_CHARACTER_LENGTH;
input.name = "poll-option";
input.placeholder = "Enter your option here";
optionEl.appendChild(input);
input.addEventListener("keydown", () => {
if (inputList.every(el => el.value) && pollOptionsAnchor) pollOptionsAnchor.appendChild(createPollOptionInput());
});
inputList.push(input);
return optionEl;
}
pollOptionsAnchor.querySelectorAll(".poll-option input").forEach(el => {
el.addEventListener("keydown", () => {
if (inputList.every(el => el.value) && pollOptionsAnchor) pollOptionsAnchor.appendChild(createPollOptionInput());
});
inputList.push(el);
});
return createPollOptionInput;
})();

4
src/Config.ts Normal file
View file

@ -0,0 +1,4 @@
"use strict";
export const MAX_POLL_OPTIONS = 255;
export const MAX_CHARACTER_LENGTH = 300;

View file

@ -5,6 +5,7 @@ import persist from "node-persist";
import { program } from "commander"; import { program } from "commander";
import { resolve } from "path"; import { resolve } from "path";
import { BackendPoll as Poll, DupeCheckMode } from "./Poll"; import { BackendPoll as Poll, DupeCheckMode } from "./Poll";
import { MAX_POLL_OPTIONS, MAX_CHARACTER_LENGTH } from "./Config";
function randomString(length = 10, charset = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789") { function randomString(length = 10, charset = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789") {
let result = ""; let result = "";
@ -69,6 +70,8 @@ export default async function init(router: Router): Promise<void> {
}): Promise<Poll | string> { }): Promise<Poll | string> {
if (!Array.isArray(pollData.options) || pollData.options.filter(i => i).length < 2) if (!Array.isArray(pollData.options) || pollData.options.filter(i => i).length < 2)
return "Options must be an array and have at least 2 entries"; return "Options must be an array and have at least 2 entries";
if (pollData.options.filter(i => i).length > MAX_POLL_OPTIONS)
return "Only " + MAX_POLL_OPTIONS + " options are allowed";
let id = randomString(8); let id = randomString(8);
while (await polls.getItem(id)) id = randomString(6); while (await polls.getItem(id)) id = randomString(6);
@ -84,10 +87,10 @@ export default async function init(router: Router): Promise<void> {
dupeCheckMode === "cookie" ? randomString(16) : null; dupeCheckMode === "cookie" ? randomString(16) : null;
const poll: Poll = { const poll: Poll = {
id, id,
title: (pollData.title || "").trim().slice(0, 300), title: (pollData.title || "").trim().slice(0, MAX_CHARACTER_LENGTH),
options: (() => { options: (() => {
const result: { [option: string]: number } = {}; const result: { [option: string]: number } = {};
for (const option of pollData.options.map(i => i.trim().slice(0, 300))) { for (const option of pollData.options.map(i => i.trim().slice(0, MAX_CHARACTER_LENGTH))) {
if (option) result[option] = 0; if (option) result[option] = 0;
} }
return result; return result;
@ -105,7 +108,7 @@ export default async function init(router: Router): Promise<void> {
router.post("/poll", async (req, res) => { router.post("/poll", async (req, res) => {
try { try {
const poll = await createPoll({ const poll = await createPoll({
title: (req.body.title || "").trim().slice(0, 300), title: (req.body.title || "").trim().slice(0, MAX_CHARACTER_LENGTH),
options: req.body.options, options: req.body.options,
dupeCheckMode: req.body.dupeCheckMode, dupeCheckMode: req.body.dupeCheckMode,
multiSelect: req.body.multiSelect || false, multiSelect: req.body.multiSelect || false,
@ -130,7 +133,7 @@ export default async function init(router: Router): Promise<void> {
router.post("/poll-form", async (req, res) => { router.post("/poll-form", async (req, res) => {
try { try {
const poll = await createPoll({ const poll = await createPoll({
title: (req.body["poll-title"] || "").trim().slice(0, 300), title: (req.body["poll-title"] || "").trim().slice(0, MAX_CHARACTER_LENGTH),
options: req.body["poll-option"], options: req.body["poll-option"],
dupeCheckMode: req.body["dupe-check"], dupeCheckMode: req.body["dupe-check"],
multiSelect: req.body["multi-select"] === "on", multiSelect: req.body["multi-select"] === "on",
@ -142,7 +145,7 @@ export default async function init(router: Router): Promise<void> {
}&title=${ }&title=${
encodeURIComponent(req.body["poll-title"]) encodeURIComponent(req.body["poll-title"])
}&options=${ }&options=${
encodeURIComponent((req.body["poll-option"] || []).join("\uFFFE")) encodeURIComponent((req.body["poll-option"] || []).slice(0, MAX_POLL_OPTIONS).join("\uFFFE"))
}&dupecheck=${ }&dupecheck=${
encodeURIComponent(req.body["dupe-check"]) encodeURIComponent(req.body["dupe-check"])
}&multiselect=${ }&multiselect=${

View file

@ -6,6 +6,7 @@ import { Router, Request, Response } from "express";
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import { program } from "commander"; import { program } from "commander";
import { FrontendPoll as Poll, PollResult } from "./Poll"; import { FrontendPoll as Poll, PollResult } from "./Poll";
import { MAX_CHARACTER_LENGTH, MAX_POLL_OPTIONS } from "./Config";
const RenderBuffer = new WeakMap(); const RenderBuffer = new WeakMap();
const RenderReplacements = new WeakMap(); const RenderReplacements = new WeakMap();
@ -134,7 +135,7 @@ export default function init(router: Router): void {
.slice(0, 3); .slice(0, 3);
const pollOptionDivs = options.map(option => ` const pollOptionDivs = options.map(option => `
<div class="poll-option"> <div class="poll-option">
<input type="text" name="poll-option" maxlength="300" placeholder="Enter your option here" value="${option}"> <input type="text" name="poll-option" maxlength="${MAX_CHARACTER_LENGTH}" placeholder="Enter your option here" value="${option}">
</div> </div>
`).join(""); `).join("");
@ -148,7 +149,9 @@ export default function init(router: Router): void {
"FORM_DUPECHECK_NONE": req.query.dupecheck === "none" ? "selected" : "", "FORM_DUPECHECK_NONE": req.query.dupecheck === "none" ? "selected" : "",
"FORM_MULTI_SELECT": req.query.multiselect === "true" ? "checked" : "", "FORM_MULTI_SELECT": req.query.multiselect === "true" ? "checked" : "",
"FORM_CAPTCHA": req.query.captcha === "true" ? "checked" : "", "FORM_CAPTCHA": req.query.captcha === "true" ? "checked" : "",
"FORM_OPTION_DIVS": pollOptionDivs "FORM_OPTION_DIVS": pollOptionDivs,
"MAX_POLL_OPTIONS": MAX_POLL_OPTIONS,
"MAX_CHARACTER_LENGTH": MAX_CHARACTER_LENGTH
}); });
}); });
} }