mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-11-23 01:50:14 +08:00
feat: add custom character generator to custom text modal (@imrajyavardhan12) (#7037)
- Add new "custom generator" button to custom text modal - Create generator modal with character input and presets - Support min/max word length and word count configuration - Include presets for alphas, numbers, symbols, bigrams, trigrams - Add code-specific patterns for programming practice Closes #6941 ### Description Adds a custom character generator to the custom text modal, allowing users to generate random "words" from a custom set of characters or strings. Useful for practicing specific character combinations, especially for programmers who want to improve typing speed with symbols and patterns commonly used in code. ### Testing 1. Open custom text modal 2. Click "custom generator" 3. Try presets or enter custom characters 4. Adjust word length and count 5. Click "Set" or "Add" --------- Co-authored-by: Amp <amp@ampcode.com> Co-authored-by: Miodec <jack@monkeytype.com>
This commit is contained in:
parent
c0191da004
commit
ffd55c532c
7 changed files with 365 additions and 11 deletions
|
|
@ -486,6 +486,8 @@
|
|||
<i class="fas fa-folder"></i>
|
||||
saved texts
|
||||
</div>
|
||||
</div>
|
||||
<div class="buttonsTop2">
|
||||
<input id="fileInput" type="file" class="hidden" accept=".txt" />
|
||||
<label for="fileInput" class="button importText">
|
||||
<i class="fas fa-file-import"></i>
|
||||
|
|
@ -495,6 +497,10 @@
|
|||
<i class="fas fa-filter"></i>
|
||||
words filter
|
||||
</div>
|
||||
<div class="button customGenerator">
|
||||
<i class="fas fa-cogs"></i>
|
||||
custom generator
|
||||
</div>
|
||||
</div>
|
||||
<div class="savedTexts hidden" style="display: none">
|
||||
<div class="title">saved texts</div>
|
||||
|
|
@ -759,7 +765,7 @@
|
|||
<select class="layoutInput"></select>
|
||||
</div>
|
||||
<!-- <div class="tip">Use the dropdowns above to generate presets</div> -->
|
||||
<button class="generateButton">generate</button>
|
||||
<button class="generateButton">apply</button>
|
||||
</div>
|
||||
|
||||
<div class="bottom">
|
||||
|
|
@ -772,6 +778,79 @@
|
|||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog id="customGeneratorModal" class="modalWrapper hidden">
|
||||
<div class="modal">
|
||||
<div class="main">
|
||||
<div class="group">
|
||||
<div class="title">presets</div>
|
||||
<select class="presetInput">
|
||||
<option value="alphas">a-z</option>
|
||||
<option value="numbers">0-9</option>
|
||||
<option value="special">symbols</option>
|
||||
<option value="bigrams">bigrams</option>
|
||||
<option value="trigrams">trigrams</option>
|
||||
</select>
|
||||
<button class="generateButton">apply</button>
|
||||
</div>
|
||||
<div class="separator"></div>
|
||||
<div class="tip">
|
||||
Enter characters or strings separated by spaces. Random combinations
|
||||
will be generated using these inputs.
|
||||
</div>
|
||||
<div class="group">
|
||||
<div class="title">character set</div>
|
||||
<textarea
|
||||
class="characterInput"
|
||||
id="characterInput"
|
||||
autocomplete="off"
|
||||
placeholder=""
|
||||
title="characters"
|
||||
></textarea>
|
||||
</div>
|
||||
<div class="group lengthgrid">
|
||||
<div class="title">min length</div>
|
||||
<div class="title">max length</div>
|
||||
|
||||
<input
|
||||
class="wordLength minLengthInput"
|
||||
autocomplete="off"
|
||||
type="number"
|
||||
value="2"
|
||||
min="1"
|
||||
title="min"
|
||||
/>
|
||||
<input
|
||||
class="wordLength maxLengthInput"
|
||||
autocomplete="off"
|
||||
type="number"
|
||||
value="5"
|
||||
min="1"
|
||||
title="max"
|
||||
/>
|
||||
</div>
|
||||
<div class="group">
|
||||
<div class="title">word count</div>
|
||||
<input
|
||||
class="wordCountInput"
|
||||
autocomplete="off"
|
||||
type="number"
|
||||
value="100"
|
||||
min="1"
|
||||
title="word count"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bottom">
|
||||
<div class="tip">
|
||||
"Set" replaces the current custom text with generated words, "Add"
|
||||
appends generated words to the current custom text.
|
||||
</div>
|
||||
<button class="setButton">set</button>
|
||||
<button class="addButton">add</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog id="googleSignUpModal" class="modalWrapper hidden">
|
||||
<form class="modal">
|
||||
<div class="title">Account name</div>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,13 @@
|
|||
#testModesNotice {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
#customTextModal {
|
||||
.modal {
|
||||
.buttonsTop2 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
#bannerCenter {
|
||||
font-size: 0.85rem;
|
||||
.banner.withImage {
|
||||
|
|
|
|||
|
|
@ -231,6 +231,7 @@
|
|||
.modal {
|
||||
grid-template-areas:
|
||||
"topButtons topButtons"
|
||||
"topButtons2 topButtons2"
|
||||
"textArea textArea"
|
||||
"checkboxes checkboxes"
|
||||
"ok ok";
|
||||
|
|
|
|||
|
|
@ -43,9 +43,9 @@
|
|||
.buttonsTop {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
textarea {
|
||||
min-height: 426px;
|
||||
}
|
||||
// textarea {
|
||||
// min-height: 426px;
|
||||
// }
|
||||
}
|
||||
}
|
||||
.testActivity {
|
||||
|
|
|
|||
|
|
@ -105,16 +105,21 @@ body.darkMode {
|
|||
// "textArea textArea checkboxes"
|
||||
// "ok ok ok";
|
||||
grid-template-areas:
|
||||
"topButtons topButtons checkboxes"
|
||||
"textArea textArea checkboxes"
|
||||
"ok ok checkboxes";
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-template-rows: min-content 1fr min-content;
|
||||
"topButtons checkboxes"
|
||||
"topButtons2 checkboxes"
|
||||
"textArea checkboxes"
|
||||
"ok checkboxes";
|
||||
grid-template-columns: auto 20rem;
|
||||
grid-template-rows: min-content min-content 1fr min-content;
|
||||
|
||||
.buttonsTop {
|
||||
grid-area: topButtons;
|
||||
}
|
||||
|
||||
.buttonsTop2 {
|
||||
grid-area: topButtons2;
|
||||
}
|
||||
|
||||
.textAreaWrapper {
|
||||
grid-area: textArea;
|
||||
}
|
||||
|
|
@ -170,7 +175,13 @@ body.darkMode {
|
|||
.buttonsTop {
|
||||
display: grid;
|
||||
// grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.buttonsTop2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
|
|
@ -199,7 +210,7 @@ body.darkMode {
|
|||
width: 100%;
|
||||
border-radius: var(--roundness);
|
||||
resize: vertical;
|
||||
min-height: 589px;
|
||||
min-height: 524px;
|
||||
color: var(--text-color);
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
|
|
@ -445,6 +456,70 @@ body.darkMode {
|
|||
}
|
||||
}
|
||||
|
||||
#customGeneratorModal {
|
||||
.modal {
|
||||
max-width: 600px;
|
||||
|
||||
.main {
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.separator {
|
||||
height: 0.25rem;
|
||||
width: 100%;
|
||||
background-color: var(--sub-alt-color);
|
||||
border-radius: var(--roundness);
|
||||
}
|
||||
|
||||
.group {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
|
||||
.title {
|
||||
color: var(--sub-color);
|
||||
}
|
||||
}
|
||||
|
||||
.lengthgrid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: auto 1fr;
|
||||
column-gap: 1rem;
|
||||
}
|
||||
|
||||
.tip {
|
||||
color: var(--sub-color);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 0.5rem;
|
||||
background: var(--sub-alt-color);
|
||||
color: var(--text-color);
|
||||
border: none;
|
||||
border-radius: var(--roundness);
|
||||
|
||||
&::placeholder {
|
||||
color: var(--sub-color);
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#quoteRateModal {
|
||||
.modal {
|
||||
max-width: 800px;
|
||||
|
|
|
|||
184
frontend/src/ts/modals/custom-generator.ts
Normal file
184
frontend/src/ts/modals/custom-generator.ts
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
import * as CustomText from "../test/custom-text";
|
||||
import * as Notifications from "../elements/notifications";
|
||||
import SlimSelect from "slim-select";
|
||||
import AnimatedModal, {
|
||||
HideOptions,
|
||||
ShowOptions,
|
||||
} from "../utils/animated-modal";
|
||||
|
||||
type Preset = {
|
||||
display: string;
|
||||
characters: string[];
|
||||
};
|
||||
|
||||
const presets: Record<string, Preset> = {
|
||||
alphas: {
|
||||
display: "a-z",
|
||||
characters: "abcdefghijklmnopqrstuvwxyz".split(""),
|
||||
},
|
||||
numbers: {
|
||||
display: "0-9",
|
||||
characters: "0123456789".split(""),
|
||||
},
|
||||
special: {
|
||||
display: "symbols",
|
||||
characters: "!@#$%^&*()_+-=[]{}|;:',.<>?/`~".split(""),
|
||||
},
|
||||
bigrams: {
|
||||
display: "bigrams",
|
||||
characters: [
|
||||
"th",
|
||||
"he",
|
||||
"in",
|
||||
"er",
|
||||
"an",
|
||||
"re",
|
||||
"on",
|
||||
"at",
|
||||
"en",
|
||||
"nd",
|
||||
"ed",
|
||||
"es",
|
||||
"or",
|
||||
"te",
|
||||
"st",
|
||||
"ar",
|
||||
"ou",
|
||||
"it",
|
||||
"al",
|
||||
"as",
|
||||
],
|
||||
},
|
||||
trigrams: {
|
||||
display: "trigrams",
|
||||
characters: [
|
||||
"the",
|
||||
"and",
|
||||
"ing",
|
||||
"ion",
|
||||
"tio",
|
||||
"ent",
|
||||
"ati",
|
||||
"for",
|
||||
"her",
|
||||
"ter",
|
||||
"ate",
|
||||
"ver",
|
||||
"all",
|
||||
"con",
|
||||
"res",
|
||||
"are",
|
||||
"rea",
|
||||
"int",
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
let _presetSelect: SlimSelect | undefined = undefined;
|
||||
|
||||
export async function show(showOptions?: ShowOptions): Promise<void> {
|
||||
void modal.show({
|
||||
...showOptions,
|
||||
beforeAnimation: async (modalEl) => {
|
||||
_presetSelect = new SlimSelect({
|
||||
select: "#customGeneratorModal .presetInput",
|
||||
settings: {
|
||||
contentLocation: modalEl,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function applyPreset(): void {
|
||||
const presetName = $("#customGeneratorModal .presetInput").val() as string;
|
||||
if (presetName !== undefined && presetName !== "" && presets[presetName]) {
|
||||
const preset = presets[presetName];
|
||||
$("#customGeneratorModal .characterInput").val(preset.characters.join(" "));
|
||||
}
|
||||
}
|
||||
|
||||
function hide(hideOptions?: HideOptions<OutgoingData>): void {
|
||||
void modal.hide({
|
||||
...hideOptions,
|
||||
});
|
||||
}
|
||||
|
||||
function generateWords(): string[] {
|
||||
const characterInput = $(
|
||||
"#customGeneratorModal .characterInput"
|
||||
).val() as string;
|
||||
const minLength =
|
||||
parseInt($("#customGeneratorModal .minLengthInput").val() as string) || 2;
|
||||
const maxLength =
|
||||
parseInt($("#customGeneratorModal .maxLengthInput").val() as string) || 5;
|
||||
const wordCount =
|
||||
parseInt($("#customGeneratorModal .wordCountInput").val() as string) || 100;
|
||||
|
||||
if (!characterInput || characterInput.trim() === "") {
|
||||
Notifications.add("Character set cannot be empty", 0);
|
||||
return [];
|
||||
}
|
||||
|
||||
const characters = characterInput.trim().split(/\s+/);
|
||||
const generatedWords: string[] = [];
|
||||
|
||||
for (let i = 0; i < wordCount; i++) {
|
||||
const wordLength =
|
||||
Math.floor(Math.random() * (maxLength - minLength + 1)) + minLength;
|
||||
let word = "";
|
||||
|
||||
for (let j = 0; j < wordLength; j++) {
|
||||
const randomChar =
|
||||
characters[Math.floor(Math.random() * characters.length)];
|
||||
word += randomChar;
|
||||
}
|
||||
|
||||
generatedWords.push(word);
|
||||
}
|
||||
|
||||
return generatedWords;
|
||||
}
|
||||
|
||||
async function apply(set: boolean): Promise<void> {
|
||||
const generatedWords = generateWords();
|
||||
|
||||
if (generatedWords.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const customText = generatedWords.join(
|
||||
CustomText.getPipeDelimiter() ? "|" : " "
|
||||
);
|
||||
|
||||
hide({
|
||||
modalChainData: {
|
||||
text: customText,
|
||||
set,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function setup(modalEl: HTMLElement): Promise<void> {
|
||||
modalEl.querySelector(".setButton")?.addEventListener("click", () => {
|
||||
void apply(true);
|
||||
});
|
||||
|
||||
modalEl.querySelector(".addButton")?.addEventListener("click", () => {
|
||||
void apply(false);
|
||||
});
|
||||
|
||||
modalEl.querySelector(".generateButton")?.addEventListener("click", () => {
|
||||
applyPreset();
|
||||
});
|
||||
}
|
||||
|
||||
type OutgoingData = {
|
||||
text: string;
|
||||
set: boolean;
|
||||
};
|
||||
|
||||
const modal = new AnimatedModal<unknown, OutgoingData>({
|
||||
dialogId: "customGeneratorModal",
|
||||
setup,
|
||||
});
|
||||
|
|
@ -6,6 +6,7 @@ import * as ChallengeController from "../controllers/challenge-controller";
|
|||
import Config, * as UpdateConfig from "../config";
|
||||
import * as Strings from "../utils/strings";
|
||||
import * as WordFilterPopup from "./word-filter";
|
||||
import * as CustomGeneratorPopup from "./custom-generator";
|
||||
import * as PractiseWords from "../test/practise-words";
|
||||
import * as Notifications from "../elements/notifications";
|
||||
import * as SavedTextsPopup from "./saved-texts";
|
||||
|
|
@ -560,6 +561,13 @@ async function setup(modalEl: HTMLElement): Promise<void> {
|
|||
modalChain: modal as AnimatedModal,
|
||||
});
|
||||
});
|
||||
modalEl
|
||||
.querySelector(".button.customGenerator")
|
||||
?.addEventListener("click", () => {
|
||||
void CustomGeneratorPopup.show({
|
||||
modalChain: modal as AnimatedModal,
|
||||
});
|
||||
});
|
||||
modalEl
|
||||
.querySelector(".button.showSavedTexts")
|
||||
?.addEventListener("click", () => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue