mirror of
https://github.com/Wolvan/poll.horse.git
synced 2025-02-16 17:54:22 +01:00
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:
parent
678342a9c6
commit
c00ea29b4f
6 changed files with 59 additions and 8 deletions
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }}
|
||||||
|
|
32
frontend/static/js/index.js
Normal file
32
frontend/static/js/index.js
Normal 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
4
src/Config.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
export const MAX_POLL_OPTIONS = 255;
|
||||||
|
export const MAX_CHARACTER_LENGTH = 300;
|
|
@ -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=${
|
||||||
|
|
|
@ -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
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
Loading…
Reference in a new issue