From c00ea29b4fb29c01fb72056f6e32c5c4b6bf3ce4 Mon Sep 17 00:00:00 2001 From: Wolvan Date: Sat, 1 Jan 2022 14:36:25 +0100 Subject: [PATCH] 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. --- frontend/.eslintrc.json | 4 ++++ frontend/html/index.html | 7 ++++++- frontend/static/js/index.js | 32 ++++++++++++++++++++++++++++++++ src/Config.ts | 4 ++++ src/backend.ts | 13 ++++++++----- src/frontend.ts | 7 +++++-- 6 files changed, 59 insertions(+), 8 deletions(-) create mode 100644 frontend/static/js/index.js create mode 100644 src/Config.ts diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json index 676509a..9bb4be6 100644 --- a/frontend/.eslintrc.json +++ b/frontend/.eslintrc.json @@ -4,5 +4,9 @@ "commonjs": false, "es6": true, "node": false + }, + "globals": { + "MAX_POLL_OPTIONS": "readonly", + "MAX_CHARACTER_LENGTH": "readonly" } } diff --git a/frontend/html/index.html b/frontend/html/index.html index c022e6a..48b2484 100644 --- a/frontend/html/index.html +++ b/frontend/html/index.html @@ -8,6 +8,11 @@ + +
@@ -22,7 +27,7 @@
- +
{{ FORM_OPTION_DIVS }} diff --git a/frontend/static/js/index.js b/frontend/static/js/index.js new file mode 100644 index 0000000..5fae1fd --- /dev/null +++ b/frontend/static/js/index.js @@ -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; +})(); \ No newline at end of file diff --git a/src/Config.ts b/src/Config.ts new file mode 100644 index 0000000..aceafac --- /dev/null +++ b/src/Config.ts @@ -0,0 +1,4 @@ +"use strict"; + +export const MAX_POLL_OPTIONS = 255; +export const MAX_CHARACTER_LENGTH = 300; \ No newline at end of file diff --git a/src/backend.ts b/src/backend.ts index 3d03f44..6ed54cf 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -5,6 +5,7 @@ import persist from "node-persist"; import { program } from "commander"; import { resolve } from "path"; import { BackendPoll as Poll, DupeCheckMode } from "./Poll"; +import { MAX_POLL_OPTIONS, MAX_CHARACTER_LENGTH } from "./Config"; function randomString(length = 10, charset = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789") { let result = ""; @@ -69,6 +70,8 @@ export default async function init(router: Router): Promise { }): Promise { if (!Array.isArray(pollData.options) || pollData.options.filter(i => i).length < 2) 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); while (await polls.getItem(id)) id = randomString(6); @@ -84,10 +87,10 @@ export default async function init(router: Router): Promise { dupeCheckMode === "cookie" ? randomString(16) : null; const poll: Poll = { id, - title: (pollData.title || "").trim().slice(0, 300), + title: (pollData.title || "").trim().slice(0, MAX_CHARACTER_LENGTH), options: (() => { 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; } return result; @@ -105,7 +108,7 @@ export default async function init(router: Router): Promise { router.post("/poll", async (req, res) => { try { 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, dupeCheckMode: req.body.dupeCheckMode, multiSelect: req.body.multiSelect || false, @@ -130,7 +133,7 @@ export default async function init(router: Router): Promise { router.post("/poll-form", async (req, res) => { try { 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"], dupeCheckMode: req.body["dupe-check"], multiSelect: req.body["multi-select"] === "on", @@ -142,7 +145,7 @@ export default async function init(router: Router): Promise { }&title=${ encodeURIComponent(req.body["poll-title"]) }&options=${ - encodeURIComponent((req.body["poll-option"] || []).join("\uFFFE")) + encodeURIComponent((req.body["poll-option"] || []).slice(0, MAX_POLL_OPTIONS).join("\uFFFE")) }&dupecheck=${ encodeURIComponent(req.body["dupe-check"]) }&multiselect=${ diff --git a/src/frontend.ts b/src/frontend.ts index b6865e4..07bfb7e 100644 --- a/src/frontend.ts +++ b/src/frontend.ts @@ -6,6 +6,7 @@ import { Router, Request, Response } from "express"; import fetch from 'node-fetch'; import { program } from "commander"; import { FrontendPoll as Poll, PollResult } from "./Poll"; +import { MAX_CHARACTER_LENGTH, MAX_POLL_OPTIONS } from "./Config"; const RenderBuffer = new WeakMap(); const RenderReplacements = new WeakMap(); @@ -134,7 +135,7 @@ export default function init(router: Router): void { .slice(0, 3); const pollOptionDivs = options.map(option => `
- +
`).join(""); @@ -148,7 +149,9 @@ export default function init(router: Router): void { "FORM_DUPECHECK_NONE": req.query.dupecheck === "none" ? "selected" : "", "FORM_MULTI_SELECT": req.query.multiselect === "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 }); }); } \ No newline at end of file