diff --git a/frontend/src/html/popups.html b/frontend/src/html/popups.html index 7ebd1760b..991cd6bc8 100644 --- a/frontend/src/html/popups.html +++ b/frontend/src/html/popups.html @@ -758,10 +758,7 @@ -
- -
-
-
+ @@ -814,10 +811,7 @@ -
- -
-
-
+
Suspected cheating -
- -
-
-
+
- +
- +

https://github.com/

- +

https://twitter.com/

- +
- +
diff --git a/frontend/src/styles/inputs.scss b/frontend/src/styles/inputs.scss index 3d38c41b4..9ca26de2f 100644 --- a/frontend/src/styles/inputs.scss +++ b/frontend/src/styles/inputs.scss @@ -15,6 +15,25 @@ textarea { } } +.textareaWithCounter { + position: relative; + + .char-counter { + position: absolute; + top: -1.75rem; + right: 0.25rem; + color: var(--sub-color); + -webkit-user-select: none; + user-select: none; + &.error { + color: var(--error-color); + } + &.warning { + color: color-mix(in srgb, var(--text-color) 50%, var(--error-color) 50%); + } + } +} + textarea { resize: vertical; } diff --git a/frontend/src/styles/popups.scss b/frontend/src/styles/popups.scss index f98d85b9a..2b46af9bd 100644 --- a/frontend/src/styles/popups.scss +++ b/frontend/src/styles/popups.scss @@ -937,17 +937,6 @@ line-height: 1.2rem; min-height: 5rem; } - .characterCount { - position: absolute; - top: -1.75rem; - right: 0.25rem; - color: var(--sub-color); - -webkit-user-select: none; - user-select: none; - &.red { - color: var(--error-color); - } - } } } #apeKeysModal { @@ -1167,18 +1156,6 @@ line-height: 1.2rem; min-height: 5rem; } - - .characterCount { - position: absolute; - top: -1.75rem; - right: 0.25rem; - color: var(--sub-color); - -webkit-user-select: none; - user-select: none; - &.red { - color: var(--error-color); - } - } } } @@ -1206,18 +1183,6 @@ line-height: 1.2rem; min-height: 5rem; } - - .characterCount { - position: absolute; - top: -1.75rem; - right: 0.25rem; - color: var(--sub-color); - -webkit-user-select: none; - user-select: none; - &.red { - color: var(--error-color); - } - } } } diff --git a/frontend/src/ts/elements/character-counter.ts b/frontend/src/ts/elements/character-counter.ts new file mode 100644 index 000000000..b118cd59a --- /dev/null +++ b/frontend/src/ts/elements/character-counter.ts @@ -0,0 +1,53 @@ +export class CharacterCounter { + private textareaElement: JQuery; + private parentElement: JQuery; + private counterElement: JQuery; + private maxLength: number; + + constructor(textareaElement: JQuery, maxLength: number) { + this.textareaElement = textareaElement; + this.maxLength = maxLength; + + this.textareaElement.attr("maxlength", this.maxLength.toString()); + + // Wrap the textarea element in a div if not already wrapped + if (!this.textareaElement.parent().hasClass("textareaWithCounter")) { + $(this.textareaElement).wrap(`
`); + } + this.parentElement = $(this.textareaElement).parent(".textareaWithCounter"); + + // Create the counter element if it doesn't exist + if (this.parentElement.find(".char-counter").length === 0) { + this.counterElement = $(``); + this.parentElement.append(this.counterElement); + } else { + this.counterElement = this.parentElement.find(".char-counter"); + } + + this.updateCounter(); + this.textareaElement.on("input", () => this.updateCounter()); + } + + private updateCounter(): void { + const maxLength = this.maxLength; + const currentLength = (this.textareaElement.val() as string).length; + const remaining = maxLength - currentLength; + this.counterElement.text(`${currentLength}/${maxLength}`); + + const remainingPercentage = (remaining / this.maxLength) * 100; + + this.counterElement.removeClass("warning"); + this.counterElement.removeClass("error"); + + if (remainingPercentage === 0) { + this.counterElement.addClass("error"); + } else if (remainingPercentage < 10) { + this.counterElement.addClass("warning"); + } + } + + public setMaxLength(maxLength: number): void { + this.maxLength = maxLength; + this.updateCounter(); + } +} diff --git a/frontend/src/ts/modals/edit-profile.ts b/frontend/src/ts/modals/edit-profile.ts index dfa73adf0..c50af841c 100644 --- a/frontend/src/ts/modals/edit-profile.ts +++ b/frontend/src/ts/modals/edit-profile.ts @@ -6,6 +6,7 @@ import * as Notifications from "../elements/notifications"; import * as ConnectionState from "../states/connection"; import AnimatedModal from "../utils/animated-modal"; import * as Profile from "../elements/profile"; +import { CharacterCounter } from "../elements/character-counter"; export function show(): void { if (!ConnectionState.get()) { @@ -18,6 +19,7 @@ export function show(): void { void modal.show({ beforeAnimation: async () => { hydrateInputs(); + initializeCharacterCounters(); }, }); } @@ -32,8 +34,10 @@ function hide(): void { }); } -const bioInput = $("#editProfileModal .bio"); -const keyboardInput = $("#editProfileModal .keyboard"); +const bioInput: JQuery = $("#editProfileModal .bio"); +const keyboardInput: JQuery = $( + "#editProfileModal .keyboard" +); const twitterInput = $("#editProfileModal .twitter"); const githubInput = $("#editProfileModal .github"); const websiteInput = $("#editProfileModal .website"); @@ -87,6 +91,11 @@ function hydrateInputs(): void { }); } +function initializeCharacterCounters(): void { + new CharacterCounter(bioInput, 250); + new CharacterCounter(keyboardInput, 75); +} + function buildUpdatesFromInputs(): SharedTypes.UserProfileDetails { const bio = (bioInput.val() ?? "") as string; const keyboard = (keyboardInput.val() ?? "") as string; diff --git a/frontend/src/ts/modals/quote-report.ts b/frontend/src/ts/modals/quote-report.ts index d8a5f24d5..7bb72ae37 100644 --- a/frontend/src/ts/modals/quote-report.ts +++ b/frontend/src/ts/modals/quote-report.ts @@ -7,6 +7,7 @@ import * as CaptchaController from "../controllers/captcha-controller"; import { removeLanguageSize } from "../utils/strings"; import SlimSelect from "slim-select"; import AnimatedModal, { ShowOptions } from "../utils/animated-modal"; +import { CharacterCounter } from "../elements/character-counter"; type State = { quoteToReport?: MonkeyTypes.Quote; @@ -42,7 +43,6 @@ export async function show( $("#quoteReportModal .quote").text(state.quoteToReport?.text as string); $("#quoteReportModal .reason").val("Grammatical error"); $("#quoteReportModal .comment").val(""); - $("#quoteReportModal .characterCount").text("-"); state.reasonSelect = new SlimSelect({ select: "#quoteReportModal .reason", @@ -50,6 +50,11 @@ export async function show( showSearch: false, }, }); + + new CharacterCounter( + $("#quoteReportModal .comment") as JQuery, + 250 + ); }, }); } @@ -115,15 +120,6 @@ async function submitReport(): Promise { } async function setup(modalEl: HTMLElement): Promise { - modalEl.querySelector(".comment")?.addEventListener("input", () => { - const len = ($("#quoteReportModal .comment").val() as string).length; - $("#quoteReportModal .characterCount").text(len); - if (len > 250) { - $("#quoteReportModal .characterCount").addClass("red"); - } else { - $("#quoteReportModal .characterCount").removeClass("red"); - } - }); modalEl.querySelector("button")?.addEventListener("click", async () => { await submitReport(); }); diff --git a/frontend/src/ts/modals/quote-submit.ts b/frontend/src/ts/modals/quote-submit.ts index b4f6e485f..370c4eb28 100644 --- a/frontend/src/ts/modals/quote-submit.ts +++ b/frontend/src/ts/modals/quote-submit.ts @@ -7,6 +7,7 @@ import * as JSONData from "../utils/json-data"; import Config from "../config"; import SlimSelect from "slim-select"; import AnimatedModal, { ShowOptions } from "../utils/animated-modal"; +import { CharacterCounter } from "../elements/character-counter"; let dropdownReady = false; async function initDropdown(): Promise { @@ -44,8 +45,6 @@ async function submitQuote(): Promise { Notifications.add("Quote submitted.", 1); $("#quoteSubmitModal .newQuoteText").val(""); $("#quoteSubmitModal .newQuoteSource").val(""); - $("#quoteSubmitModal .characterCount").removeClass("red"); - $("#quoteSubmitModal .characterCount").text("-"); CaptchaController.reset("submitQuote"); } @@ -70,6 +69,11 @@ export async function show(showOptions: ShowOptions): Promise { ); $("#quoteSubmitModal .newQuoteLanguage").trigger("change"); $("#quoteSubmitModal input").val(""); + + new CharacterCounter( + $("#quoteSubmitModal .newQuoteText") as JQuery, + 250 + ); }, }); } @@ -86,15 +90,6 @@ function hide(clearModalChain: boolean): void { } async function setup(modalEl: HTMLElement): Promise { - modalEl.querySelector("textarea")?.addEventListener("input", (e) => { - const len = (e.target as HTMLTextAreaElement).value.length; - $("#quoteSubmitModal .characterCount").text(len); - if (len < 60) { - $("#quoteSubmitModal .characterCount").addClass("red"); - } else { - $("#quoteSubmitModal .characterCount").removeClass("red"); - } - }); modalEl.querySelector("button")?.addEventListener("click", () => { void submitQuote(); hide(true); diff --git a/frontend/src/ts/modals/user-report.ts b/frontend/src/ts/modals/user-report.ts index 864d9b2eb..400fc6f54 100644 --- a/frontend/src/ts/modals/user-report.ts +++ b/frontend/src/ts/modals/user-report.ts @@ -5,6 +5,7 @@ import * as CaptchaController from "../controllers/captcha-controller"; import SlimSelect from "slim-select"; import AnimatedModal from "../utils/animated-modal"; import { isAuthenticated } from "../firebase"; +import { CharacterCounter } from "../elements/character-counter"; type State = { userUid?: string; @@ -46,8 +47,6 @@ export async function show(options: ShowOptions): Promise { (modalEl.querySelector(".reason") as HTMLSelectElement).value = "Inappropriate name"; (modalEl.querySelector(".comment") as HTMLTextAreaElement).value = ""; - (modalEl.querySelector(".characterCount") as HTMLElement).textContent = - "-"; select = new SlimSelect({ select: modalEl.querySelector(".reason") as HTMLElement, @@ -58,6 +57,11 @@ export async function show(options: ShowOptions): Promise { }); }, }); + + new CharacterCounter( + $("#userReportModal .comment") as JQuery, + 250 + ); } async function hide(): Promise { @@ -128,19 +132,5 @@ const modal = new AnimatedModal({ modalEl.querySelector("button")?.addEventListener("click", () => { void submitReport(); }); - modalEl.querySelector(".comment")?.addEventListener("input", (e) => { - setTimeout(() => { - const len = (e.target as HTMLTextAreaElement).value.length; - const characterCount = modalEl.querySelector( - ".characterCount" - ) as HTMLElement; - characterCount.textContent = len.toString(); - if (len > 250) { - characterCount.classList.add("red"); - } else { - characterCount.classList.remove("red"); - } - }, 1); - }); }, });