mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2026-01-04 06:24:07 +08:00
refactor: custom text modal rework (#5278)
* start work on saved texts popup * passing options through, async fill to not block animation * handle apply * custom text modal start * full save custom text modal refactor * remove import * full finish on saved texts modal * fill when chain re-shows it * fix issues where opening chains within chains would show two modals * extract type * add support for passing data between modals * dont leave the popup if no words were found * use modalchaindata instead of setting popup state directly * handling set and append modes in chain data * move tab insertion code into the custom text modal file * use chain data instead of state module * move event handlers to a different file / to the setup function * move file * rename file * rename id * remove unused line * add generics to incoming and outgoing modal chain data * rework the way custom text textarea is handled * update long custom text warning * remove variable * remember to update the state
This commit is contained in:
parent
3cbf0bda4c
commit
ff7816aac0
21 changed files with 826 additions and 885 deletions
|
|
@ -367,8 +367,8 @@
|
|||
<div class="button">ok</div>
|
||||
</div>
|
||||
</dialog>
|
||||
<div id="customTextPopupWrapper" class="popupWrapper hidden">
|
||||
<div id="customTextPopup" action="">
|
||||
<dialog id="customTextModal" class="modalWrapper hidden">
|
||||
<div class="modal" action="">
|
||||
<div class="buttonsTop">
|
||||
<div class="button saveCustomText">
|
||||
<i class="fas fa-save"></i>
|
||||
|
|
@ -394,10 +394,15 @@
|
|||
</div>
|
||||
<div class="textAreaWrapper" style="position: relative">
|
||||
<div class="longCustomTextWarning">
|
||||
<div class="textAndButton">
|
||||
A long custom text is currently loaded. Editing the text will disable
|
||||
progress tracking.
|
||||
<div class="button">Got it</div>
|
||||
<div>
|
||||
<p>
|
||||
A long custom text is currently loaded.
|
||||
<br />
|
||||
Editing the text will disable progress tracking.
|
||||
<br />
|
||||
<br />
|
||||
</p>
|
||||
<p class="small">Click anywhere to start editing the text.</p>
|
||||
</div>
|
||||
</div>
|
||||
<textarea class="textarea"></textarea>
|
||||
|
|
@ -466,17 +471,17 @@
|
|||
</div>
|
||||
<div class="button apply">ok</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="savedTextsPopupWrapper" class="popupWrapper hidden">
|
||||
<div id="savedTextsPopup">
|
||||
</dialog>
|
||||
<dialog id="savedTextsModal" class="modalWrapper hidden">
|
||||
<div class="modal">
|
||||
<div class="title">Saved texts</div>
|
||||
<div class="list"></div>
|
||||
<div class="title">Saved long texts</div>
|
||||
<div class="listLong"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="saveCustomTextPopupWrapper" class="popupWrapper hidden">
|
||||
<div id="saveCustomTextPopup">
|
||||
</dialog>
|
||||
<dialog id="saveCustomTextModal" class="modalWrapper hidden">
|
||||
<form class="modal">
|
||||
<div class="title">Save custom text</div>
|
||||
<input class="textName" type="text" placeholder="name" />
|
||||
<div>
|
||||
|
|
@ -490,9 +495,9 @@
|
|||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="button save">save</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="save">save</button>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog id="wordFilterModal" class="modalWrapper hidden">
|
||||
<div class="modal">
|
||||
<!-- hidden for now, dunno where to place it -->
|
||||
|
|
|
|||
|
|
@ -50,16 +50,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
#customTextPopupWrapper {
|
||||
#customTextPopup {
|
||||
background: var(--bg-color);
|
||||
border-radius: var(--roundness);
|
||||
padding: 2rem;
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
#customTextModal {
|
||||
.modal {
|
||||
max-width: 1200px;
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
grid-template-areas:
|
||||
"topButtons topButtons topButtons"
|
||||
"textArea textArea checkboxes"
|
||||
|
|
@ -108,7 +101,8 @@
|
|||
}
|
||||
|
||||
.longCustomTextWarning {
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
// background: rgba(0, 0, 0, 0.5);
|
||||
background: var(--sub-alt-color);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
|
@ -117,12 +111,16 @@
|
|||
display: grid;
|
||||
place-items: center center;
|
||||
border-radius: var(--roundness);
|
||||
.textAndButton {
|
||||
width: 80%;
|
||||
max-width: 20rem;
|
||||
text-align: center;
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
p {
|
||||
font-size: 1.25em;
|
||||
margin: 0;
|
||||
}
|
||||
p.small {
|
||||
font-size: 0.75em;
|
||||
color: var(--sub-color);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -196,23 +194,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
#savedTextsPopupWrapper {
|
||||
#savedTextsPopup {
|
||||
color: var(--sub-color);
|
||||
background: var(--bg-color);
|
||||
border-radius: var(--roundness);
|
||||
padding: 2rem;
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
width: 400px;
|
||||
max-height: 80vh;
|
||||
overflow: auto;
|
||||
|
||||
.title {
|
||||
font-size: 1.5rem;
|
||||
color: var(--sub-color);
|
||||
}
|
||||
|
||||
#savedTextsModal {
|
||||
.modal {
|
||||
max-width: 500px;
|
||||
.list {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
|
|
@ -229,10 +213,10 @@
|
|||
.listLong {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
.savedText {
|
||||
.savedLongText {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
grid-template-columns: 2fr 1fr 3rem;
|
||||
grid-template-columns: 2fr auto auto;
|
||||
.button .fas {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
|
@ -241,21 +225,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
#saveCustomTextPopupWrapper {
|
||||
#saveCustomTextPopup {
|
||||
background: var(--bg-color);
|
||||
border-radius: var(--roundness);
|
||||
padding: 2rem;
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
width: 400px;
|
||||
max-height: 80vh;
|
||||
overflow: auto;
|
||||
|
||||
.title {
|
||||
font-size: 1.5rem;
|
||||
color: var(--sub-color);
|
||||
}
|
||||
#saveCustomTextModal {
|
||||
.modal {
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,9 +39,9 @@
|
|||
#app {
|
||||
grid-template-columns: auto;
|
||||
}
|
||||
#customTextPopupWrapper {
|
||||
#customTextModal {
|
||||
padding: 2rem;
|
||||
#customTextPopup {
|
||||
.modal {
|
||||
grid-template-columns: 1fr 1fr 2fr;
|
||||
width: 100%;
|
||||
}
|
||||
|
|
@ -57,7 +57,7 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
#customTextPopupWrapper #customTextPopup .buttonsTop {
|
||||
#customTextModal .modal .buttonsTop {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
|
@ -153,7 +153,7 @@
|
|||
|
||||
//900px
|
||||
@media only screen and (max-width: 56.25rem) {
|
||||
#customTextPopupWrapper #customTextPopup {
|
||||
#customTextModal .modal {
|
||||
grid-template-areas:
|
||||
"topButtons topButtons topButtons"
|
||||
"textArea textArea textArea"
|
||||
|
|
@ -390,7 +390,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
#customTextPopupWrapper #customTextPopup {
|
||||
#customTextModal .modal {
|
||||
.buttonsTop {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ import Config, * as UpdateConfig from "../config";
|
|||
import * as Misc from "../utils/misc";
|
||||
import * as JSONData from "../utils/json-data";
|
||||
import { randomizeTheme } from "../controllers/theme-controller";
|
||||
import * as CustomTextPopup from "../popups/custom-text-popup";
|
||||
import * as CustomTextPopup from "../modals/custom-text";
|
||||
import * as Settings from "../pages/settings";
|
||||
import * as Notifications from "../elements/notifications";
|
||||
import * as VideoAdPopup from "../popups/video-ad-popup";
|
||||
|
|
|
|||
|
|
@ -249,7 +249,6 @@ export async function setup(challengeName: string): Promise<boolean> {
|
|||
UpdateConfig.setDifficulty("normal", true);
|
||||
} else if (challenge.type === "customText") {
|
||||
CustomText.setDelimiter(" ");
|
||||
CustomText.setPopupTextareaState(challenge.parameters[0] as string, true);
|
||||
CustomText.setText((challenge.parameters[0] as string).split(" "));
|
||||
CustomText.setIsTimeRandom(false);
|
||||
CustomText.setIsSectionRandom(false);
|
||||
|
|
@ -270,7 +269,6 @@ export async function setup(challengeName: string): Promise<boolean> {
|
|||
text = text.replace(/[\n\r\t ]/gm, " ");
|
||||
text = text.replace(/ +/gm, " ");
|
||||
CustomText.setDelimiter(" ");
|
||||
CustomText.setPopupTextareaState(text, true);
|
||||
CustomText.setText(text.split(" "));
|
||||
CustomText.setIsWordRandom(false);
|
||||
CustomText.setIsSectionRandom(false);
|
||||
|
|
|
|||
|
|
@ -770,27 +770,6 @@ function handleTab(event: JQuery.KeyDownEvent, popupVisible: boolean): void {
|
|||
return;
|
||||
}
|
||||
|
||||
//special case for inserting tab characters into the textarea
|
||||
if ($("#customTextPopup .textarea").is(":focus")) {
|
||||
event.preventDefault();
|
||||
|
||||
const area = $("#customTextPopup .textarea")[0] as HTMLTextAreaElement;
|
||||
|
||||
const start: number = area.selectionStart;
|
||||
const end: number = area.selectionEnd;
|
||||
|
||||
// set textarea value to: text before caret + tab + text after caret
|
||||
area.value =
|
||||
area.value.substring(0, start) + "\t" + area.value.substring(end);
|
||||
|
||||
// put caret at right position again
|
||||
area.selectionStart = area.selectionEnd = start + 1;
|
||||
|
||||
CustomText.setPopupTextareaState(area.value);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let shouldInsertTabCharacter = false;
|
||||
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import * as Notifications from "../elements/notifications";
|
|||
import * as QuoteRateModal from "../modals/quote-rate";
|
||||
import * as QuoteReportModal from "../modals/quote-report";
|
||||
import * as QuoteSearchModal from "../modals/quote-search";
|
||||
import * as CustomTextModal from "../modals/custom-text";
|
||||
|
||||
$(".pageTest").on("click", "#testModesNotice .textButton", async (event) => {
|
||||
const attr = $(event.currentTarget).attr("commands");
|
||||
|
|
@ -69,3 +70,7 @@ $(".pageTest").on("click", "#testConfig .quoteLength .textButton", (e) => {
|
|||
void QuoteSearchModal.show();
|
||||
}
|
||||
});
|
||||
|
||||
$(".pageTest").on("click", "#testConfig .customText .textButton", () => {
|
||||
CustomTextModal.show();
|
||||
});
|
||||
|
|
|
|||
453
frontend/src/ts/modals/custom-text.ts
Normal file
453
frontend/src/ts/modals/custom-text.ts
Normal file
|
|
@ -0,0 +1,453 @@
|
|||
import * as CustomText from "../test/custom-text";
|
||||
import * as CustomTextState from "../states/custom-text-name";
|
||||
import * as ManualRestart from "../test/manual-restart-tracker";
|
||||
import * as TestLogic from "../test/test-logic";
|
||||
import * as ChallengeController from "../controllers/challenge-controller";
|
||||
import Config, * as UpdateConfig from "../config";
|
||||
import * as Misc from "../utils/misc";
|
||||
import * as WordFilterPopup from "./word-filter";
|
||||
import * as Notifications from "../elements/notifications";
|
||||
import * as SavedTextsPopup from "./saved-texts";
|
||||
import * as SaveCustomTextPopup from "./save-custom-text";
|
||||
import AnimatedModal from "../utils/animated-modal";
|
||||
|
||||
const popup = "#customTextModal .modal";
|
||||
|
||||
type State = {
|
||||
textarea: string;
|
||||
lastSavedTextareaState: string;
|
||||
};
|
||||
|
||||
const state: State = {
|
||||
textarea: CustomText.text.join(" "),
|
||||
lastSavedTextareaState: CustomText.text.join(" "),
|
||||
};
|
||||
|
||||
function updateLongTextWarning(): void {
|
||||
if (CustomTextState.isCustomTextLong() === true) {
|
||||
$(`${popup} .longCustomTextWarning`).removeClass("hidden");
|
||||
$(`${popup} .randomWordsCheckbox input`).prop("checked", false);
|
||||
$(`${popup} .delimiterCheck input`).prop("checked", false);
|
||||
$(`${popup} .typographyCheck`).prop("checked", true);
|
||||
$(`${popup} .replaceNewlineWithSpace input`).prop("checked", false);
|
||||
$(`${popup} .inputs`).addClass("disabled");
|
||||
} else {
|
||||
$(`${popup} .longCustomTextWarning`).addClass("hidden");
|
||||
$(`${popup} .inputs`).removeClass("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
//todo: rewrite this file to use a state object instead of constantly directly accessing the DOM
|
||||
|
||||
async function beforeAnimation(
|
||||
modalEl: HTMLElement,
|
||||
modalChainData?: IncomingData
|
||||
): Promise<void> {
|
||||
updateLongTextWarning();
|
||||
|
||||
if (
|
||||
CustomText.isSectionRandom ||
|
||||
CustomText.isTimeRandom ||
|
||||
CustomText.isWordRandom
|
||||
) {
|
||||
$(`${popup} .randomWordsCheckbox input`).prop("checked", true);
|
||||
} else {
|
||||
$(`${popup} .randomWordsCheckbox input`).prop("checked", false);
|
||||
}
|
||||
|
||||
if (CustomText.delimiter === "|") {
|
||||
$(`${popup} .delimiterCheck input`).prop("checked", true);
|
||||
} else {
|
||||
$(`${popup} .delimiterCheck input`).prop("checked", false);
|
||||
}
|
||||
|
||||
if ($(`${popup} .randomWordsCheckbox input`).prop("checked") as boolean) {
|
||||
$(`${popup} .inputs .randomInputFields`).removeClass("disabled");
|
||||
} else {
|
||||
$(`${popup} .inputs .randomInputFields`).addClass("disabled");
|
||||
}
|
||||
if ($(`${popup} .replaceNewlineWithSpace input`).prop("checked") as boolean) {
|
||||
$(`${popup} .inputs .replaceNewLinesButtons`).removeClass("disabled");
|
||||
} else {
|
||||
$(`${popup} .inputs .replaceNewLinesButtons`).addClass("disabled");
|
||||
}
|
||||
|
||||
if (CustomTextState.isCustomTextLong()) {
|
||||
// if we are in long custom text mode, always reset the textarea state to the current text
|
||||
state.textarea = CustomText.text.join(" ");
|
||||
}
|
||||
|
||||
if (modalChainData?.text !== undefined) {
|
||||
if (modalChainData.long !== true && CustomTextState.isCustomTextLong()) {
|
||||
CustomTextState.setCustomTextName("", undefined);
|
||||
Notifications.add("Disabled long custom text progress tracking", 0, {
|
||||
duration: 5,
|
||||
});
|
||||
updateLongTextWarning();
|
||||
}
|
||||
|
||||
const newText =
|
||||
modalChainData.set ?? true
|
||||
? modalChainData.text
|
||||
: state.textarea + " " + modalChainData.text;
|
||||
state.textarea = newText;
|
||||
}
|
||||
$(`${popup} textarea`).val(state.textarea);
|
||||
|
||||
$(`${popup} .wordcount input`).val(
|
||||
CustomText.word === -1 ? "" : CustomText.word
|
||||
);
|
||||
$(`${popup} .time input`).val(CustomText.time === -1 ? "" : CustomText.time);
|
||||
}
|
||||
|
||||
async function afterAnimation(): Promise<void> {
|
||||
if (!CustomTextState.isCustomTextLong()) {
|
||||
$(`${popup} textarea`).trigger("focus");
|
||||
}
|
||||
}
|
||||
|
||||
export function show(): void {
|
||||
state.textarea = state.lastSavedTextareaState;
|
||||
void modal.show({
|
||||
beforeAnimation,
|
||||
afterAnimation,
|
||||
});
|
||||
}
|
||||
|
||||
function hide(): void {
|
||||
void modal.hide();
|
||||
}
|
||||
|
||||
function handleDelimiterChange(): void {
|
||||
let delimiter;
|
||||
if ($(`${popup} .delimiterCheck input`).prop("checked") as boolean) {
|
||||
delimiter = "|";
|
||||
|
||||
$(`${popup} .randomInputFields .sectioncount `).removeClass("hidden");
|
||||
|
||||
$(`${popup} .randomInputFields .wordcount input `).val("");
|
||||
$(`${popup} .randomInputFields .wordcount `).addClass("hidden");
|
||||
} else {
|
||||
delimiter = " ";
|
||||
$(`${popup} .randomInputFields .sectioncount input `).val("");
|
||||
$(`${popup} .randomInputFields .sectioncount `).addClass("hidden");
|
||||
$(`${popup} .randomInputFields .wordcount `).removeClass("hidden");
|
||||
}
|
||||
if (
|
||||
$(`${popup} textarea`).val() !== CustomText.text.join(CustomText.delimiter)
|
||||
) {
|
||||
const currentText = $(`${popup} textarea`).val() as string;
|
||||
const currentTextSplit = currentText.split(CustomText.delimiter);
|
||||
let newtext = currentTextSplit.join(delimiter);
|
||||
newtext = newtext.replace(/\n /g, "\n");
|
||||
$(`${popup} textarea`).val(newtext);
|
||||
state.textarea = newtext;
|
||||
} else {
|
||||
let newtext = CustomText.text.join(delimiter);
|
||||
newtext = newtext.replace(/\n /g, "\n");
|
||||
$(`${popup} textarea`).val(newtext);
|
||||
state.textarea = newtext;
|
||||
}
|
||||
CustomText.setDelimiter(delimiter);
|
||||
}
|
||||
|
||||
function handleFileOpen(): void {
|
||||
const file = ($(`#fileInput`)[0] as HTMLInputElement).files?.[0];
|
||||
if (file) {
|
||||
if (file.type !== "text/plain") {
|
||||
Notifications.add("File is not a text file", -1, {
|
||||
duration: 5,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(file, "UTF-8");
|
||||
|
||||
reader.onload = (readerEvent): void => {
|
||||
const content = readerEvent.target?.result as string;
|
||||
$(`${popup} textarea`).val(content);
|
||||
state.textarea = content;
|
||||
$(`#fileInput`).val("");
|
||||
};
|
||||
reader.onerror = (): void => {
|
||||
Notifications.add("Failed to read file", -1, {
|
||||
duration: 5,
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function apply(): void {
|
||||
let text = state.textarea;
|
||||
|
||||
if (text === "") {
|
||||
Notifications.add("Text cannot be empty", 0);
|
||||
return;
|
||||
}
|
||||
|
||||
state.lastSavedTextareaState = state.textarea;
|
||||
|
||||
text = text.normalize().trim();
|
||||
// text = text.replace(/[\r]/gm, " ");
|
||||
|
||||
//replace any characters that look like a space with an actual space
|
||||
text = text.replace(/[\u2000-\u200A\u202F\u205F\u00A0]/g, " ");
|
||||
|
||||
//replace zero width characters
|
||||
text = text.replace(/[\u200B-\u200D\u2060\uFEFF]/g, "");
|
||||
|
||||
if (
|
||||
$(`${popup} .replaceControlCharacters input`).prop("checked") as boolean
|
||||
) {
|
||||
text = text.replace(/([^\\]|^)\\t/gm, "$1\t");
|
||||
text = text.replace(/([^\\]|^)\\n/gm, "$1\n");
|
||||
text = text.replace(/\\\\t/gm, "\\t");
|
||||
text = text.replace(/\\\\n/gm, "\\n");
|
||||
}
|
||||
|
||||
text = text.replace(/ +/gm, " ");
|
||||
text = text.replace(/( *(\r\n|\r|\n) *)/g, "\n ");
|
||||
if ($(`${popup} .typographyCheck input`).prop("checked") as boolean) {
|
||||
text = Misc.cleanTypographySymbols(text);
|
||||
}
|
||||
if ($(`${popup} .replaceNewlineWithSpace input`).prop("checked") as boolean) {
|
||||
let periods = true;
|
||||
if (
|
||||
$(
|
||||
$(`${popup} .replaceNewLinesButtons .button`)[0] as HTMLElement
|
||||
).hasClass("active")
|
||||
) {
|
||||
periods = false;
|
||||
}
|
||||
|
||||
if (periods) {
|
||||
text = text.replace(/\n/gm, ". ");
|
||||
text = text.replace(/\.\. /gm, ". ");
|
||||
text = text.replace(/ +/gm, " ");
|
||||
} else {
|
||||
text = text.replace(/\n/gm, " ");
|
||||
text = text.replace(/ +/gm, " ");
|
||||
}
|
||||
}
|
||||
|
||||
const words = text.split(CustomText.delimiter).filter((word) => word !== "");
|
||||
CustomText.setText(words);
|
||||
|
||||
CustomText.setWord(
|
||||
parseInt(($(`${popup} .wordcount input`).val() as string) || "-1")
|
||||
);
|
||||
CustomText.setTime(
|
||||
parseInt(($(`${popup} .time input`).val() as string) || "-1")
|
||||
);
|
||||
|
||||
CustomText.setSection(
|
||||
parseInt(($(`${popup} .sectioncount input`).val() as string) || "-1")
|
||||
);
|
||||
CustomText.setIsWordRandom(
|
||||
($(`${popup} .randomWordsCheckbox input`).prop("checked") as boolean) &&
|
||||
CustomText.word > -1
|
||||
);
|
||||
CustomText.setIsTimeRandom(
|
||||
($(`${popup} .randomWordsCheckbox input`).prop("checked") as boolean) &&
|
||||
CustomText.time > -1
|
||||
);
|
||||
CustomText.setIsSectionRandom(
|
||||
($(`${popup} .randomWordsCheckbox input`).prop("checked") as boolean) &&
|
||||
CustomText.section > -1
|
||||
);
|
||||
if (
|
||||
($(`${popup} .randomWordsCheckbox input`).prop("checked") as boolean) &&
|
||||
!CustomText.isTimeRandom &&
|
||||
!CustomText.isWordRandom &&
|
||||
!CustomText.isSectionRandom
|
||||
) {
|
||||
Notifications.add(
|
||||
"You need to specify word count or time in seconds to start a random custom test",
|
||||
0,
|
||||
{
|
||||
duration: 5,
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
($(`${popup} .randomWordsCheckbox input`).prop("checked") as boolean) &&
|
||||
CustomText.isTimeRandom &&
|
||||
CustomText.isWordRandom
|
||||
) {
|
||||
Notifications.add(
|
||||
"You need to pick between word count or time in seconds to start a random custom test",
|
||||
0,
|
||||
{
|
||||
duration: 5,
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(CustomText.isWordRandom && CustomText.word === 0) ||
|
||||
(CustomText.isTimeRandom && CustomText.time === 0)
|
||||
) {
|
||||
Notifications.add(
|
||||
"Infinite words! Make sure to use Bail Out from the command line to save your result.",
|
||||
0,
|
||||
{
|
||||
duration: 7,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
ChallengeController.clearActive();
|
||||
ManualRestart.set();
|
||||
if (Config.mode !== "custom") UpdateConfig.setMode("custom");
|
||||
TestLogic.restart();
|
||||
hide();
|
||||
}
|
||||
|
||||
async function setup(modalEl: HTMLElement): Promise<void> {
|
||||
modalEl
|
||||
.querySelector(".delimiterCheck input")
|
||||
?.addEventListener("change", handleDelimiterChange);
|
||||
modalEl
|
||||
.querySelector("#fileInput")
|
||||
?.addEventListener("change", handleFileOpen);
|
||||
modalEl
|
||||
.querySelector(".randomWordsCheckbox input")
|
||||
?.addEventListener("change", () => {
|
||||
if ($(`${popup} .randomWordsCheckbox input`).prop("checked") as boolean) {
|
||||
$(`${popup} .inputs .randomInputFields`).removeClass("disabled");
|
||||
} else {
|
||||
$(`${popup} .inputs .randomInputFields`).addClass("disabled");
|
||||
}
|
||||
});
|
||||
modalEl
|
||||
.querySelector(".replaceNewlineWithSpace input")
|
||||
?.addEventListener("change", () => {
|
||||
if (
|
||||
$(`${popup} .replaceNewlineWithSpace input`).prop("checked") as boolean
|
||||
) {
|
||||
$(`${popup} .inputs .replaceNewLinesButtons`).removeClass("disabled");
|
||||
} else {
|
||||
$(`${popup} .inputs .replaceNewLinesButtons`).addClass("disabled");
|
||||
}
|
||||
});
|
||||
const replaceNewLinesButtons = modalEl.querySelectorAll(
|
||||
".replaceNewLinesButtons .button"
|
||||
);
|
||||
for (const button of replaceNewLinesButtons) {
|
||||
button.addEventListener("click", () => {
|
||||
$(`${popup} .replaceNewLinesButtons .button`).removeClass("active");
|
||||
$(button).addClass("active");
|
||||
});
|
||||
}
|
||||
const textarea = modalEl.querySelector("textarea");
|
||||
textarea?.addEventListener("input", (e) => {
|
||||
state.textarea = (e.target as HTMLTextAreaElement).value;
|
||||
});
|
||||
textarea?.addEventListener("keydown", (e) => {
|
||||
if (e.key !== "Tab") return;
|
||||
e.preventDefault();
|
||||
|
||||
const area = e.target as HTMLTextAreaElement;
|
||||
const start: number = area.selectionStart;
|
||||
const end: number = area.selectionEnd;
|
||||
|
||||
// set textarea value to: text before caret + tab + text after caret
|
||||
area.value =
|
||||
area.value.substring(0, start) + "\t" + area.value.substring(end);
|
||||
|
||||
// put caret at right position again
|
||||
area.selectionStart = area.selectionEnd = start + 1;
|
||||
|
||||
state.textarea = area.value;
|
||||
});
|
||||
textarea?.addEventListener("keypress", (e) => {
|
||||
if (Misc.isElementVisible(`#customTextModal .longCustomTextWarning`)) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (e.code === "Enter" && e.ctrlKey) {
|
||||
$(`${popup} .button.apply`).trigger("click");
|
||||
}
|
||||
if (
|
||||
CustomTextState.isCustomTextLong() &&
|
||||
CustomTextState.getCustomTextName() !== ""
|
||||
) {
|
||||
CustomTextState.setCustomTextName("", undefined);
|
||||
Notifications.add("Disabled long custom text progress tracking", 0, {
|
||||
duration: 5,
|
||||
});
|
||||
updateLongTextWarning();
|
||||
}
|
||||
});
|
||||
modalEl
|
||||
.querySelector(".randomInputFields .wordcount input")
|
||||
?.addEventListener("keypress", (e) => {
|
||||
$(`${popup} .randomInputFields .time input`).val("");
|
||||
$(`${popup} .randomInputFields .sectioncount input`).val("");
|
||||
});
|
||||
modalEl
|
||||
.querySelector(".randomInputFields .time input")
|
||||
?.addEventListener("keypress", (e) => {
|
||||
$(`${popup} .randomInputFields .wordcount input`).val("");
|
||||
$(`${popup} .randomInputFields .sectioncount input`).val("");
|
||||
});
|
||||
modalEl
|
||||
.querySelector(".randomInputFields .sectioncount input")
|
||||
?.addEventListener("keypress", (e) => {
|
||||
$(`${popup} .randomInputFields .time input`).val("");
|
||||
$(`${popup} .randomInputFields .wordcount input`).val("");
|
||||
});
|
||||
modalEl.querySelector(".button.apply")?.addEventListener("click", () => {
|
||||
apply();
|
||||
});
|
||||
modalEl.querySelector(".button.wordfilter")?.addEventListener("click", () => {
|
||||
void WordFilterPopup.show({
|
||||
modalChain: modal as AnimatedModal<unknown, unknown>,
|
||||
});
|
||||
});
|
||||
modalEl
|
||||
.querySelector(".button.showSavedTexts")
|
||||
?.addEventListener("click", () => {
|
||||
void SavedTextsPopup.show({
|
||||
modalChain: modal as AnimatedModal<unknown, unknown>,
|
||||
});
|
||||
});
|
||||
modalEl
|
||||
.querySelector(".button.saveCustomText")
|
||||
?.addEventListener("click", () => {
|
||||
void SaveCustomTextPopup.show({
|
||||
modalChain: modal as AnimatedModal<unknown, unknown>,
|
||||
modalChainData: { text: state.textarea },
|
||||
});
|
||||
});
|
||||
modalEl
|
||||
.querySelector(".longCustomTextWarning")
|
||||
?.addEventListener("click", () => {
|
||||
$(`#customTextModal .longCustomTextWarning`).addClass("hidden");
|
||||
});
|
||||
}
|
||||
|
||||
type IncomingData = {
|
||||
text: string;
|
||||
set?: boolean;
|
||||
long?: boolean;
|
||||
};
|
||||
|
||||
const modal = new AnimatedModal<IncomingData>({
|
||||
dialogId: "customTextModal",
|
||||
setup,
|
||||
customEscapeHandler: async (): Promise<void> => {
|
||||
hide();
|
||||
},
|
||||
customWrapperClickHandler: async (): Promise<void> => {
|
||||
hide();
|
||||
},
|
||||
showOptionsWhenInChain: {
|
||||
beforeAnimation,
|
||||
afterAnimation,
|
||||
},
|
||||
});
|
||||
|
|
@ -4,7 +4,7 @@ import * as ManualRestart from "../test/manual-restart-tracker";
|
|||
import * as CustomWordAmountPopup from "./custom-word-amount";
|
||||
import * as CustomTestDurationPopup from "./custom-test-duration";
|
||||
import * as QuoteSearchModal from "./quote-search";
|
||||
import * as CustomTextPopup from "../popups/custom-text-popup";
|
||||
import * as CustomTextPopup from "./custom-text";
|
||||
import AnimatedModal from "../utils/animated-modal";
|
||||
|
||||
function update(): void {
|
||||
|
|
|
|||
121
frontend/src/ts/modals/save-custom-text.ts
Normal file
121
frontend/src/ts/modals/save-custom-text.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import * as CustomText from "../test/custom-text";
|
||||
import * as Notifications from "../elements/notifications";
|
||||
import * as CustomTextState from "../states/custom-text-name";
|
||||
import { InputIndicator } from "../elements/input-indicator";
|
||||
import { debounce } from "throttle-debounce";
|
||||
import AnimatedModal, { ShowOptions } from "../utils/animated-modal";
|
||||
|
||||
let indicator: InputIndicator | undefined;
|
||||
|
||||
type State = {
|
||||
textToSave: string;
|
||||
};
|
||||
|
||||
const state: State = {
|
||||
textToSave: "",
|
||||
};
|
||||
|
||||
export async function show(options: ShowOptions<IncomingData>): Promise<void> {
|
||||
state.textToSave = "";
|
||||
void modal.show({
|
||||
...options,
|
||||
beforeAnimation: async (modalEl, modalChainData) => {
|
||||
state.textToSave = modalChainData?.text ?? "";
|
||||
$("#saveCustomTextModal .textName").val("");
|
||||
$("#saveCustomTextModal .isLongText").prop("checked", false);
|
||||
$("#saveCustomTextModal button.save").prop("disabled", true);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function hide(): void {
|
||||
void modal.hide();
|
||||
}
|
||||
|
||||
function save(): boolean {
|
||||
const name = $("#saveCustomTextModal .textName").val() as string;
|
||||
const checkbox = $("#saveCustomTextModal .isLongText").prop("checked");
|
||||
|
||||
if (!name) {
|
||||
Notifications.add("Custom text needs a name", 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (state.textToSave.length === 0) {
|
||||
Notifications.add("Custom text can't be empty", 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
state.textToSave = state.textToSave.replace(/( *(\r\n|\r|\n) *)/g, "\n ");
|
||||
|
||||
CustomText.setCustomText(name, state.textToSave, checkbox);
|
||||
CustomTextState.setCustomTextName(name, checkbox);
|
||||
Notifications.add("Custom text saved", 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateIndicatorAndButton(): void {
|
||||
const val = $("#saveCustomTextModal .textName").val() as string;
|
||||
const checkbox = $("#saveCustomTextModal .isLongText").prop("checked");
|
||||
|
||||
if (!val) {
|
||||
indicator?.hide();
|
||||
$("#saveCustomTextModal button.save").addClass("disabled");
|
||||
} else {
|
||||
const names = CustomText.getCustomTextNames(checkbox);
|
||||
if (names.includes(val)) {
|
||||
indicator?.show("unavailable");
|
||||
$("#saveCustomTextModal button.save").prop("disabled", true);
|
||||
} else {
|
||||
indicator?.show("available");
|
||||
$("#saveCustomTextModal button.save").prop("disabled", false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updateInputAndButtonDebounced = debounce(500, updateIndicatorAndButton);
|
||||
|
||||
async function setup(modalEl: HTMLElement): Promise<void> {
|
||||
indicator = new InputIndicator($("#saveCustomTextModal .textName"), {
|
||||
available: {
|
||||
icon: "fa-check",
|
||||
level: 1,
|
||||
},
|
||||
unavailable: {
|
||||
icon: "fa-times",
|
||||
level: -1,
|
||||
},
|
||||
loading: {
|
||||
icon: "fa-circle-notch",
|
||||
spinIcon: true,
|
||||
level: 0,
|
||||
},
|
||||
});
|
||||
modalEl.addEventListener("submit", (e) => {
|
||||
e.preventDefault();
|
||||
if (save()) hide();
|
||||
});
|
||||
modalEl.querySelector(".textName")?.addEventListener("input", (e) => {
|
||||
const val = (e.target as HTMLInputElement).value;
|
||||
if (val.length > 0) {
|
||||
indicator?.show("loading");
|
||||
updateInputAndButtonDebounced();
|
||||
}
|
||||
});
|
||||
modalEl.querySelector(".isLongText")?.addEventListener("input", (e) => {
|
||||
const val = (e.target as HTMLInputElement).value;
|
||||
if (val.length > 0) {
|
||||
indicator?.show("loading");
|
||||
updateInputAndButtonDebounced();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
type IncomingData = {
|
||||
text: string;
|
||||
};
|
||||
|
||||
const modal = new AnimatedModal<IncomingData>({
|
||||
dialogId: "saveCustomTextModal",
|
||||
setup,
|
||||
});
|
||||
133
frontend/src/ts/modals/saved-texts.ts
Normal file
133
frontend/src/ts/modals/saved-texts.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import * as CustomText from "../test/custom-text";
|
||||
import * as CustomTextState from "../states/custom-text-name";
|
||||
import { escapeHTML } from "../utils/misc";
|
||||
import AnimatedModal, {
|
||||
HideOptions,
|
||||
ShowOptions,
|
||||
} from "../utils/animated-modal";
|
||||
import { showPopup } from "./simple-modals";
|
||||
|
||||
async function fill(): Promise<void> {
|
||||
const names = CustomText.getCustomTextNames();
|
||||
const listEl = $(`#savedTextsModal .list`).empty();
|
||||
let list = "";
|
||||
if (names.length === 0) {
|
||||
list += "<div>No saved custom texts found</div>";
|
||||
} else {
|
||||
for (const name of names) {
|
||||
list += `<div class="savedText" data-name="${name}">
|
||||
<div class="button name">${escapeHTML(name)}</div>
|
||||
<div class="button delete">
|
||||
<i class="fas fa-fw fa-trash"></i>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
listEl.html(list);
|
||||
|
||||
const longNames = CustomText.getCustomTextNames(true);
|
||||
const longListEl = $(`#savedTextsModal .listLong`).empty();
|
||||
let longList = "";
|
||||
if (longNames.length === 0) {
|
||||
longList += "<div>No saved long custom texts found</div>";
|
||||
} else {
|
||||
for (const name of longNames) {
|
||||
longList += `<div class="savedLongText" data-name="${name}">
|
||||
<div class="button name">${escapeHTML(name)}</div>
|
||||
<div class="button ${
|
||||
CustomText.getCustomTextLongProgress(name) <= 0 ? "disabled" : ""
|
||||
} resetProgress">reset</div>
|
||||
<div class="button delete">
|
||||
<i class="fas fa-fw fa-trash"></i>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
longListEl.html(longList);
|
||||
|
||||
$("#savedTextsModal .list .savedText .button.delete").on("click", (e) => {
|
||||
const name = $(e.target).closest(".savedText").data("name");
|
||||
showPopup("deleteCustomText", [name], {
|
||||
modalChain: modal as AnimatedModal<unknown, unknown>,
|
||||
});
|
||||
});
|
||||
|
||||
$("#savedTextsModal .listLong .savedLongText .button.delete").on(
|
||||
"click",
|
||||
(e) => {
|
||||
const name = $(e.target).closest(".savedLongText").data("name");
|
||||
showPopup("deleteCustomTextLong", [name], {
|
||||
modalChain: modal as AnimatedModal<unknown, unknown>,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
$("#savedTextsModal .listLong .savedLongText .button.resetProgress").on(
|
||||
"click",
|
||||
(e) => {
|
||||
const name = $(e.target).closest(".savedLongText").data("name");
|
||||
showPopup("resetProgressCustomTextLong", [name], {
|
||||
modalChain: modal as AnimatedModal<unknown, unknown>,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
$("#savedTextsModal .list .savedText .button.name").on("click", (e) => {
|
||||
const name = $(e.target).text();
|
||||
CustomTextState.setCustomTextName(name, false);
|
||||
const text = getSavedText(name, false);
|
||||
hide({ modalChainData: { text, long: false } });
|
||||
});
|
||||
|
||||
$("#savedTextsModal .listLong .savedLongText .button.name").on(
|
||||
"click",
|
||||
(e) => {
|
||||
const name = $(e.target).text();
|
||||
CustomTextState.setCustomTextName(name, true);
|
||||
const text = getSavedText(name, true);
|
||||
hide({ modalChainData: { text, long: true } });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function show(options: ShowOptions): Promise<void> {
|
||||
void modal.show({
|
||||
...options,
|
||||
beforeAnimation: async () => {
|
||||
void fill();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function hide(hideOptions?: HideOptions<OutgoingData>): void {
|
||||
void modal.hide({
|
||||
...hideOptions,
|
||||
});
|
||||
}
|
||||
|
||||
function getSavedText(name: string, long: boolean): string {
|
||||
let text = CustomText.getCustomText(name, long);
|
||||
if (long) {
|
||||
text = text.slice(CustomText.getCustomTextLongProgress(name));
|
||||
}
|
||||
return text.join(" ");
|
||||
}
|
||||
|
||||
async function setup(): Promise<void> {
|
||||
//
|
||||
}
|
||||
|
||||
type OutgoingData = {
|
||||
text: string;
|
||||
long: boolean;
|
||||
};
|
||||
|
||||
const modal = new AnimatedModal<unknown, OutgoingData>({
|
||||
dialogId: "savedTextsModal",
|
||||
setup,
|
||||
showOptionsWhenInChain: {
|
||||
beforeAnimation: async (): Promise<void> => {
|
||||
void fill();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -7,7 +7,6 @@ import * as Notifications from "../elements/notifications";
|
|||
import * as Settings from "../pages/settings";
|
||||
import * as ThemePicker from "../settings/theme-picker";
|
||||
import * as CustomText from "../test/custom-text";
|
||||
import * as SavedTextsPopup from "../popups/saved-texts-popup";
|
||||
import * as AccountButton from "../elements/account-button";
|
||||
import { FirebaseError } from "firebase/app";
|
||||
import { Auth, isAuthenticated, getAuthenticatedUser } from "../firebase";
|
||||
|
|
@ -1450,7 +1449,6 @@ list.deleteCustomText = new SimpleModal({
|
|||
execFn: async (_thisPopup): Promise<ExecReturn> => {
|
||||
CustomText.deleteCustomText(_thisPopup.parameters[0] as string, false);
|
||||
CustomTextState.setCustomTextName("", undefined);
|
||||
void SavedTextsPopup.show(true);
|
||||
|
||||
return {
|
||||
status: 1,
|
||||
|
|
@ -1471,7 +1469,6 @@ list.deleteCustomTextLong = new SimpleModal({
|
|||
execFn: async (_thisPopup): Promise<ExecReturn> => {
|
||||
CustomText.deleteCustomText(_thisPopup.parameters[0] as string, true);
|
||||
CustomTextState.setCustomTextName("", undefined);
|
||||
void SavedTextsPopup.show(true);
|
||||
|
||||
return {
|
||||
status: 1,
|
||||
|
|
@ -1491,12 +1488,11 @@ list.resetProgressCustomTextLong = new SimpleModal({
|
|||
buttonText: "reset",
|
||||
execFn: async (_thisPopup): Promise<ExecReturn> => {
|
||||
CustomText.setCustomTextLongProgress(_thisPopup.parameters[0] as string, 0);
|
||||
void SavedTextsPopup.show(true);
|
||||
CustomText.setPopupTextareaState(
|
||||
CustomText.getCustomText(_thisPopup.parameters[0] as string, true).join(
|
||||
" "
|
||||
)
|
||||
const text = CustomText.getCustomText(
|
||||
_thisPopup.parameters[0] as string,
|
||||
true
|
||||
);
|
||||
CustomText.setText(text);
|
||||
return {
|
||||
status: 1,
|
||||
message: "Custom text progress reset",
|
||||
|
|
@ -1746,33 +1742,6 @@ $(".pageSettings").on(
|
|||
}
|
||||
);
|
||||
|
||||
$("#popups").on(
|
||||
"click",
|
||||
`#savedTextsPopupWrapper .list .savedText .button.delete`,
|
||||
(e) => {
|
||||
const name = $(e.target).siblings(".button.name").text();
|
||||
showPopup("deleteCustomText", [name]);
|
||||
}
|
||||
);
|
||||
|
||||
$("#popups").on(
|
||||
"click",
|
||||
`#savedTextsPopupWrapper .listLong .savedText .button.delete`,
|
||||
(e) => {
|
||||
const name = $(e.target).siblings(".button.name").text();
|
||||
showPopup("deleteCustomTextLong", [name]);
|
||||
}
|
||||
);
|
||||
|
||||
$("#popups").on(
|
||||
"click",
|
||||
`#savedTextsPopupWrapper .listLong .savedText .button.resetProgress`,
|
||||
(e) => {
|
||||
const name = $(e.target).siblings(".button.name").text();
|
||||
showPopup("resetProgressCustomTextLong", [name]);
|
||||
}
|
||||
);
|
||||
|
||||
$(".pageSettings").on(
|
||||
"click",
|
||||
".section[data-config-name='fontFamily'] button[data-config-value='custom']",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@ import * as JSONData from "../utils/json-data";
|
|||
import * as CustomText from "../test/custom-text";
|
||||
import * as Notifications from "../elements/notifications";
|
||||
import SlimSelect from "slim-select";
|
||||
import AnimatedModal, { ShowOptions } from "../utils/animated-modal";
|
||||
import AnimatedModal, {
|
||||
HideOptions,
|
||||
ShowOptions,
|
||||
} from "../utils/animated-modal";
|
||||
|
||||
type FilterPreset = {
|
||||
display: string;
|
||||
|
|
@ -179,9 +182,9 @@ export async function show(showOptions?: ShowOptions): Promise<void> {
|
|||
});
|
||||
}
|
||||
|
||||
function hide(clearModalChain?: boolean): void {
|
||||
function hide(hideOptions?: HideOptions<OutgoingData>): void {
|
||||
void modal.hide({
|
||||
clearModalChain,
|
||||
...hideOptions,
|
||||
afterAnimation: async () => {
|
||||
languageSelect?.destroy();
|
||||
layoutSelect?.destroy();
|
||||
|
|
@ -249,16 +252,18 @@ async function apply(set: boolean): Promise<void> {
|
|||
|
||||
if (filteredWords.length === 0) {
|
||||
Notifications.add("No words found", 0);
|
||||
hide(true);
|
||||
enableButtons();
|
||||
return;
|
||||
}
|
||||
|
||||
const customText = filteredWords.join(CustomText.delimiter);
|
||||
|
||||
CustomText.setPopupTextareaState(
|
||||
(set ? "" : CustomText.popupTextareaState + " ") + customText
|
||||
);
|
||||
hide(true);
|
||||
hide({
|
||||
modalChainData: {
|
||||
text: customText,
|
||||
set,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function disableButtons(): void {
|
||||
|
|
@ -319,7 +324,12 @@ async function setup(): Promise<void> {
|
|||
});
|
||||
}
|
||||
|
||||
const modal = new AnimatedModal({
|
||||
type OutgoingData = {
|
||||
text: string;
|
||||
set: boolean;
|
||||
};
|
||||
|
||||
const modal = new AnimatedModal<unknown, OutgoingData>({
|
||||
dialogId: "wordFilterModal",
|
||||
setup,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,418 +0,0 @@
|
|||
import * as CustomText from "../test/custom-text";
|
||||
import * as CustomTextState from "../states/custom-text-name";
|
||||
import * as ManualRestart from "../test/manual-restart-tracker";
|
||||
import * as TestLogic from "../test/test-logic";
|
||||
import * as ChallengeController from "../controllers/challenge-controller";
|
||||
import Config, * as UpdateConfig from "../config";
|
||||
import * as Misc from "../utils/misc";
|
||||
import * as WordFilterPopup from "../modals/word-filter";
|
||||
import * as Notifications from "../elements/notifications";
|
||||
import * as SavedTextsPopup from "./saved-texts-popup";
|
||||
import * as SaveCustomTextPopup from "./save-custom-text-popup";
|
||||
import * as Skeleton from "../utils/skeleton";
|
||||
|
||||
const skeletonId = "customTextPopupWrapper";
|
||||
|
||||
const wrapper = "#customTextPopupWrapper";
|
||||
const popup = "#customTextPopup";
|
||||
|
||||
function updateLongTextWarning(): void {
|
||||
if (CustomTextState.isCustomTextLong() === true) {
|
||||
$(`${popup} .longCustomTextWarning`).removeClass("hidden");
|
||||
$(`${popup} .randomWordsCheckbox input`).prop("checked", false);
|
||||
$(`${popup} .delimiterCheck input`).prop("checked", false);
|
||||
$(`${popup} .typographyCheck`).prop("checked", true);
|
||||
$(`${popup} .replaceNewlineWithSpace input`).prop("checked", false);
|
||||
$(`${popup} .inputs`).addClass("disabled");
|
||||
} else {
|
||||
$(`${popup} .longCustomTextWarning`).addClass("hidden");
|
||||
$(`${popup} .inputs`).removeClass("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
//todo: rewrite this file to use a state object instead of constantly directly accessing the DOM
|
||||
|
||||
export function show(noAnim = false): void {
|
||||
Skeleton.append(skeletonId, "popups");
|
||||
if (!Misc.isElementVisible(wrapper)) {
|
||||
updateLongTextWarning();
|
||||
|
||||
if (
|
||||
CustomText.isSectionRandom ||
|
||||
CustomText.isTimeRandom ||
|
||||
CustomText.isWordRandom
|
||||
) {
|
||||
$(`${popup} .randomWordsCheckbox input`).prop("checked", true);
|
||||
} else {
|
||||
$(`${popup} .randomWordsCheckbox input`).prop("checked", false);
|
||||
}
|
||||
|
||||
if (CustomText.delimiter === "|") {
|
||||
$(`${popup} .delimiterCheck input`).prop("checked", true);
|
||||
} else {
|
||||
$(`${popup} .delimiterCheck input`).prop("checked", false);
|
||||
}
|
||||
|
||||
if ($(`${popup} .randomWordsCheckbox input`).prop("checked") as boolean) {
|
||||
$(`${popup} .inputs .randomInputFields`).removeClass("disabled");
|
||||
} else {
|
||||
$(`${popup} .inputs .randomInputFields`).addClass("disabled");
|
||||
}
|
||||
if (
|
||||
$(`${popup} .replaceNewlineWithSpace input`).prop("checked") as boolean
|
||||
) {
|
||||
$(`${popup} .inputs .replaceNewLinesButtons`).removeClass("disabled");
|
||||
} else {
|
||||
$(`${popup} .inputs .replaceNewLinesButtons`).addClass("disabled");
|
||||
}
|
||||
$(`${popup} textarea`).val(CustomText.popupTextareaState);
|
||||
|
||||
$(wrapper)
|
||||
.stop(true, true)
|
||||
.css("opacity", 0)
|
||||
.removeClass("hidden")
|
||||
.animate({ opacity: 1 }, noAnim ? 0 : 125, () => {
|
||||
$(`${popup} .wordcount input`).val(
|
||||
CustomText.word === -1 ? "" : CustomText.word
|
||||
);
|
||||
$(`${popup} .time input`).val(
|
||||
CustomText.time === -1 ? "" : CustomText.time
|
||||
);
|
||||
$(`${popup} textarea`).trigger("focus");
|
||||
});
|
||||
}
|
||||
setTimeout(
|
||||
() => {
|
||||
if (!CustomTextState.isCustomTextLong()) {
|
||||
$(`${popup} textarea`).trigger("focus");
|
||||
}
|
||||
},
|
||||
noAnim ? 10 : 150
|
||||
);
|
||||
}
|
||||
|
||||
$(`${popup} .delimiterCheck input`).on("change", () => {
|
||||
let delimiter;
|
||||
if ($(`${popup} .delimiterCheck input`).prop("checked") as boolean) {
|
||||
delimiter = "|";
|
||||
|
||||
$(`${popup} .randomInputFields .sectioncount `).removeClass("hidden");
|
||||
|
||||
$(`${popup} .randomInputFields .wordcount input `).val("");
|
||||
$(`${popup} .randomInputFields .wordcount `).addClass("hidden");
|
||||
} else {
|
||||
delimiter = " ";
|
||||
$(`${popup} .randomInputFields .sectioncount input `).val("");
|
||||
$(`${popup} .randomInputFields .sectioncount `).addClass("hidden");
|
||||
$(`${popup} .randomInputFields .wordcount `).removeClass("hidden");
|
||||
}
|
||||
if (
|
||||
$(`${popup} textarea`).val() !== CustomText.text.join(CustomText.delimiter)
|
||||
) {
|
||||
const currentText = $(`${popup} textarea`).val() as string;
|
||||
const currentTextSplit = currentText.split(CustomText.delimiter);
|
||||
let newtext = currentTextSplit.join(delimiter);
|
||||
newtext = newtext.replace(/\n /g, "\n");
|
||||
$(`${popup} textarea`).val(newtext);
|
||||
} else {
|
||||
let newtext = CustomText.text.join(delimiter);
|
||||
newtext = newtext.replace(/\n /g, "\n");
|
||||
$(`${popup} textarea`).val(newtext);
|
||||
}
|
||||
CustomText.setDelimiter(delimiter);
|
||||
});
|
||||
|
||||
type HideOptions = {
|
||||
noAnim?: boolean | undefined;
|
||||
resetState?: boolean | undefined;
|
||||
};
|
||||
|
||||
function hide(options = {} as HideOptions): void {
|
||||
if (options.noAnim === undefined) options.noAnim = false;
|
||||
if (options.resetState === undefined) options.resetState = true;
|
||||
|
||||
if (Misc.isElementVisible(wrapper)) {
|
||||
$(wrapper)
|
||||
.stop(true, true)
|
||||
.css("opacity", 1)
|
||||
.animate(
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
options.noAnim ? 0 : 125,
|
||||
() => {
|
||||
if (options.resetState) {
|
||||
CustomText.setPopupTextareaStateToSaved();
|
||||
}
|
||||
|
||||
$(wrapper).addClass("hidden");
|
||||
Skeleton.remove(skeletonId);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$(wrapper).on("mousedown", (e) => {
|
||||
if ($(e.target).attr("id") === "customTextPopupWrapper") {
|
||||
hide();
|
||||
}
|
||||
});
|
||||
|
||||
$(`${popup} .inputs .randomWordsCheckbox input`).on("change", () => {
|
||||
if ($(`${popup} .randomWordsCheckbox input`).prop("checked") as boolean) {
|
||||
$(`${popup} .inputs .randomInputFields`).removeClass("disabled");
|
||||
} else {
|
||||
$(`${popup} .inputs .randomInputFields`).addClass("disabled");
|
||||
}
|
||||
});
|
||||
|
||||
$(`${popup} .replaceNewlineWithSpace input`).on("change", () => {
|
||||
if ($(`${popup} .replaceNewlineWithSpace input`).prop("checked") as boolean) {
|
||||
$(`${popup} .inputs .replaceNewLinesButtons`).removeClass("disabled");
|
||||
} else {
|
||||
$(`${popup} .inputs .replaceNewLinesButtons`).addClass("disabled");
|
||||
}
|
||||
});
|
||||
|
||||
$(`${popup} .inputs .replaceNewLinesButtons .button`).on("click", (e) => {
|
||||
$(`${popup} .inputs .replaceNewLinesButtons .button`).removeClass("active");
|
||||
$(e.target).addClass("active");
|
||||
});
|
||||
|
||||
$(`${popup} textarea`).on("input", () => {
|
||||
CustomText.setPopupTextareaState($(`${popup} textarea`).val() as string);
|
||||
});
|
||||
|
||||
$(`${popup} textarea`).on("keypress", (e) => {
|
||||
if (Misc.isElementVisible(`#customTextPopup .longCustomTextWarning`)) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (e.code === "Enter" && e.ctrlKey) {
|
||||
$(`${popup} .button.apply`).trigger("click");
|
||||
}
|
||||
if (
|
||||
CustomTextState.isCustomTextLong() &&
|
||||
CustomTextState.getCustomTextName() !== ""
|
||||
) {
|
||||
CustomTextState.setCustomTextName("", undefined);
|
||||
Notifications.add("Disabled long custom text progress tracking", 0, {
|
||||
duration: 5,
|
||||
});
|
||||
updateLongTextWarning();
|
||||
}
|
||||
});
|
||||
|
||||
$(`${popup} .randomInputFields .wordcount input`).on("keypress", () => {
|
||||
$(`${popup} .randomInputFields .time input`).val("");
|
||||
$(`${popup} .randomInputFields .sectioncount input`).val("");
|
||||
});
|
||||
|
||||
$(`${popup} .randomInputFields .time input`).on("keypress", () => {
|
||||
$(`${popup} .randomInputFields .wordcount input`).val("");
|
||||
$(`${popup} .randomInputFields .sectioncount input`).val("");
|
||||
});
|
||||
|
||||
$(`${popup} .randomInputFields .sectioncount input`).on("keypress", () => {
|
||||
$(`${popup} .randomInputFields .time input`).val("");
|
||||
$(`${popup} .randomInputFields .wordcount input`).val("");
|
||||
});
|
||||
|
||||
function apply(): void {
|
||||
let text = ($(`${popup} textarea`).val() as string).normalize();
|
||||
|
||||
if (text === "") {
|
||||
Notifications.add("Text cannot be empty", 0);
|
||||
return;
|
||||
}
|
||||
|
||||
CustomText.setPopupTextareaState(text, true);
|
||||
|
||||
text = text.trim();
|
||||
// text = text.replace(/[\r]/gm, " ");
|
||||
|
||||
//replace any characters that look like a space with an actual space
|
||||
text = text.replace(/[\u2000-\u200A\u202F\u205F\u00A0]/g, " ");
|
||||
|
||||
//replace zero width characters
|
||||
text = text.replace(/[\u200B-\u200D\u2060\uFEFF]/g, "");
|
||||
|
||||
if (
|
||||
$(`${popup} .replaceControlCharacters input`).prop("checked") as boolean
|
||||
) {
|
||||
text = text.replace(/([^\\]|^)\\t/gm, "$1\t");
|
||||
text = text.replace(/([^\\]|^)\\n/gm, "$1\n");
|
||||
text = text.replace(/\\\\t/gm, "\\t");
|
||||
text = text.replace(/\\\\n/gm, "\\n");
|
||||
}
|
||||
|
||||
text = text.replace(/ +/gm, " ");
|
||||
text = text.replace(/( *(\r\n|\r|\n) *)/g, "\n ");
|
||||
if ($(`${popup} .typographyCheck input`).prop("checked") as boolean) {
|
||||
text = Misc.cleanTypographySymbols(text);
|
||||
}
|
||||
if ($(`${popup} .replaceNewlineWithSpace input`).prop("checked") as boolean) {
|
||||
let periods = true;
|
||||
if (
|
||||
$(
|
||||
$(`${popup} .replaceNewLinesButtons .button`)[0] as HTMLElement
|
||||
).hasClass("active")
|
||||
) {
|
||||
periods = false;
|
||||
}
|
||||
|
||||
if (periods) {
|
||||
text = text.replace(/\n/gm, ". ");
|
||||
text = text.replace(/\.\. /gm, ". ");
|
||||
text = text.replace(/ +/gm, " ");
|
||||
} else {
|
||||
text = text.replace(/\n/gm, " ");
|
||||
text = text.replace(/ +/gm, " ");
|
||||
}
|
||||
}
|
||||
|
||||
const words = text.split(CustomText.delimiter).filter((word) => word !== "");
|
||||
CustomText.setText(words);
|
||||
|
||||
CustomText.setWord(
|
||||
parseInt(($(`${popup} .wordcount input`).val() as string) || "-1")
|
||||
);
|
||||
CustomText.setTime(
|
||||
parseInt(($(`${popup} .time input`).val() as string) || "-1")
|
||||
);
|
||||
|
||||
CustomText.setSection(
|
||||
parseInt(($(`${popup} .sectioncount input`).val() as string) || "-1")
|
||||
);
|
||||
CustomText.setIsWordRandom(
|
||||
($(`${popup} .randomWordsCheckbox input`).prop("checked") as boolean) &&
|
||||
CustomText.word > -1
|
||||
);
|
||||
CustomText.setIsTimeRandom(
|
||||
($(`${popup} .randomWordsCheckbox input`).prop("checked") as boolean) &&
|
||||
CustomText.time > -1
|
||||
);
|
||||
CustomText.setIsSectionRandom(
|
||||
($(`${popup} .randomWordsCheckbox input`).prop("checked") as boolean) &&
|
||||
CustomText.section > -1
|
||||
);
|
||||
if (
|
||||
($(`${popup} .randomWordsCheckbox input`).prop("checked") as boolean) &&
|
||||
!CustomText.isTimeRandom &&
|
||||
!CustomText.isWordRandom &&
|
||||
!CustomText.isSectionRandom
|
||||
) {
|
||||
Notifications.add(
|
||||
"You need to specify word count or time in seconds to start a random custom test",
|
||||
0,
|
||||
{
|
||||
duration: 5,
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
($(`${popup} .randomWordsCheckbox input`).prop("checked") as boolean) &&
|
||||
CustomText.isTimeRandom &&
|
||||
CustomText.isWordRandom
|
||||
) {
|
||||
Notifications.add(
|
||||
"You need to pick between word count or time in seconds to start a random custom test",
|
||||
0,
|
||||
{
|
||||
duration: 5,
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(CustomText.isWordRandom && CustomText.word === 0) ||
|
||||
(CustomText.isTimeRandom && CustomText.time === 0)
|
||||
) {
|
||||
Notifications.add(
|
||||
"Infinite words! Make sure to use Bail Out from the command line to save your result.",
|
||||
0,
|
||||
{
|
||||
duration: 7,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
ChallengeController.clearActive();
|
||||
ManualRestart.set();
|
||||
if (Config.mode !== "custom") UpdateConfig.setMode("custom");
|
||||
TestLogic.restart();
|
||||
hide();
|
||||
}
|
||||
|
||||
$("#popups").on("click", `${popup} .button.apply`, () => {
|
||||
apply();
|
||||
});
|
||||
|
||||
$(".pageTest").on("click", "#testConfig .customText .textButton", () => {
|
||||
show();
|
||||
});
|
||||
|
||||
$(document).on("keydown", (event) => {
|
||||
if (
|
||||
event.key === "Escape" &&
|
||||
Misc.isElementVisible("#customTextPopupWrapper")
|
||||
) {
|
||||
hide();
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
$("#popups").on("click", `${popup} .wordfilter`, () => {
|
||||
hide({ noAnim: true, resetState: false });
|
||||
//todo use modal chain
|
||||
void WordFilterPopup.show();
|
||||
});
|
||||
|
||||
$(`${popup} .buttonsTop .showSavedTexts`).on("click", () => {
|
||||
hide({ noAnim: true });
|
||||
void SavedTextsPopup.show(true, () => {
|
||||
show(true);
|
||||
});
|
||||
});
|
||||
|
||||
$(`#customTextPopupWrapper .buttonsTop .saveCustomText`).on("click", () => {
|
||||
hide({ noAnim: true, resetState: false });
|
||||
void SaveCustomTextPopup.show(true, () => {
|
||||
show(true);
|
||||
});
|
||||
});
|
||||
|
||||
$(`#customTextPopupWrapper .longCustomTextWarning .button`).on("click", () => {
|
||||
$(`#customTextPopup .longCustomTextWarning`).addClass("hidden");
|
||||
});
|
||||
|
||||
$(`#fileInput`).on("change", () => {
|
||||
const file = ($(`#fileInput`)[0] as HTMLInputElement).files?.[0];
|
||||
if (file) {
|
||||
if (file.type !== "text/plain") {
|
||||
Notifications.add("File is not a text file", -1, {
|
||||
duration: 5,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(file, "UTF-8");
|
||||
|
||||
reader.onload = (readerEvent): void => {
|
||||
const content = readerEvent.target?.result as string;
|
||||
$(`${popup} textarea`).val(content);
|
||||
$(`#fileInput`).val("");
|
||||
};
|
||||
reader.onerror = (): void => {
|
||||
Notifications.add("Failed to read file", -1, {
|
||||
duration: 5,
|
||||
});
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
Skeleton.save(skeletonId);
|
||||
|
|
@ -1,138 +0,0 @@
|
|||
import * as CustomText from "../test/custom-text";
|
||||
import * as Notifications from "../elements/notifications";
|
||||
import * as CustomTextState from "../states/custom-text-name";
|
||||
import { InputIndicator } from "../elements/input-indicator";
|
||||
import { debounce } from "throttle-debounce";
|
||||
import * as Skeleton from "../utils/skeleton";
|
||||
import { isPopupVisible } from "../utils/misc";
|
||||
|
||||
const wrapperId = "saveCustomTextPopupWrapper";
|
||||
|
||||
const indicator = new InputIndicator($("#saveCustomTextPopup .textName"), {
|
||||
available: {
|
||||
icon: "fa-check",
|
||||
level: 1,
|
||||
},
|
||||
unavailable: {
|
||||
icon: "fa-times",
|
||||
level: -1,
|
||||
},
|
||||
loading: {
|
||||
icon: "fa-circle-notch",
|
||||
spinIcon: true,
|
||||
level: 0,
|
||||
},
|
||||
});
|
||||
|
||||
let callbackFuncOnHide: (() => void) | undefined = undefined;
|
||||
|
||||
export async function show(
|
||||
noAnim = false,
|
||||
callbackOnHide: () => void | undefined
|
||||
): Promise<void> {
|
||||
Skeleton.append(wrapperId, "popups");
|
||||
if (!isPopupVisible(wrapperId)) {
|
||||
callbackFuncOnHide = callbackOnHide;
|
||||
|
||||
$("#saveCustomTextPopupWrapper")
|
||||
.stop(true, true)
|
||||
.css("opacity", 0)
|
||||
.removeClass("hidden")
|
||||
.animate({ opacity: 1 }, noAnim ? 0 : 125, () => {
|
||||
$("#saveCustomTextPopupWrapper .textName").val("");
|
||||
$("#saveCustomTextPopupWrapper .isLongText").prop("checked", false);
|
||||
$("#saveCustomTextPopupWrapper .button.save").addClass("disabled");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function hide(noAnim = false): void {
|
||||
if (isPopupVisible(wrapperId)) {
|
||||
$("#saveCustomTextPopupWrapper")
|
||||
.stop(true, true)
|
||||
.css("opacity", 1)
|
||||
.animate(
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
noAnim ? 0 : 125,
|
||||
() => {
|
||||
$("#saveCustomTextPopupWrapper").addClass("hidden");
|
||||
Skeleton.remove(wrapperId);
|
||||
if (callbackFuncOnHide) callbackFuncOnHide();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function save(): boolean {
|
||||
const name = $("#saveCustomTextPopupWrapper .textName").val() as string;
|
||||
const checkbox = $("#saveCustomTextPopupWrapper .isLongText").prop("checked");
|
||||
let text = CustomText.popupTextareaState.normalize();
|
||||
|
||||
if (!name) {
|
||||
Notifications.add("Custom text needs a name", 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (text.length === 0) {
|
||||
Notifications.add("Custom text can't be empty", 0);
|
||||
return false;
|
||||
}
|
||||
|
||||
text = text.replace(/( *(\r\n|\r|\n) *)/g, "\n ");
|
||||
|
||||
CustomText.setCustomText(name, text, checkbox);
|
||||
CustomTextState.setCustomTextName(name, checkbox);
|
||||
Notifications.add("Custom text saved", 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
$("#popups").on("click", `#saveCustomTextPopupWrapper .button.save`, () => {
|
||||
if (save()) hide(true);
|
||||
});
|
||||
|
||||
$("#saveCustomTextPopupWrapper").on("mousedown", (e) => {
|
||||
if ($(e.target).attr("id") === "saveCustomTextPopupWrapper") {
|
||||
hide(true);
|
||||
}
|
||||
});
|
||||
|
||||
function updateIndicatorAndButton(): void {
|
||||
const val = $("#saveCustomTextPopup .textName").val() as string;
|
||||
const checkbox = $("#saveCustomTextPopupWrapper .isLongText").prop("checked");
|
||||
|
||||
if (!val) {
|
||||
indicator.hide();
|
||||
$("#saveCustomTextPopupWrapper .button.save").addClass("disabled");
|
||||
} else {
|
||||
const names = CustomText.getCustomTextNames(checkbox);
|
||||
if (names.includes(val)) {
|
||||
indicator.show("unavailable");
|
||||
$("#saveCustomTextPopupWrapper .button.save").addClass("disabled");
|
||||
} else {
|
||||
indicator.show("available");
|
||||
$("#saveCustomTextPopupWrapper .button.save").removeClass("disabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updateInputAndButtonDebounced = debounce(500, updateIndicatorAndButton);
|
||||
|
||||
$("#saveCustomTextPopup .textName").on("input", () => {
|
||||
const val = $("#saveCustomTextPopup .textName").val() as string;
|
||||
if (val.length > 0) {
|
||||
indicator.show("loading");
|
||||
updateInputAndButtonDebounced();
|
||||
}
|
||||
});
|
||||
|
||||
$("#saveCustomTextPopupWrapper .isLongText").on("change", () => {
|
||||
const val = $("#saveCustomTextPopup .textName").val() as string;
|
||||
if (val.length > 0) {
|
||||
indicator.show("loading");
|
||||
updateInputAndButtonDebounced();
|
||||
}
|
||||
});
|
||||
|
||||
Skeleton.save(wrapperId);
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
import * as CustomText from "../test/custom-text";
|
||||
import * as CustomTextState from "../states/custom-text-name";
|
||||
import * as Skeleton from "../utils/skeleton";
|
||||
import { escapeHTML, isPopupVisible } from "../utils/misc";
|
||||
|
||||
const wrapperId = "savedTextsPopupWrapper";
|
||||
|
||||
function fill(): void {
|
||||
const names = CustomText.getCustomTextNames();
|
||||
const listEl = $(`#savedTextsPopup .list`).empty();
|
||||
let list = "";
|
||||
if (names.length === 0) {
|
||||
list += "<div>No saved custom texts found</div>";
|
||||
} else {
|
||||
for (const name of names) {
|
||||
list += `<div class="savedText">
|
||||
<div class="button name">${escapeHTML(name)}</div>
|
||||
<div class="button delete">
|
||||
<i class="fas fa-fw fa-trash"></i>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
listEl.html(list);
|
||||
|
||||
const longNames = CustomText.getCustomTextNames(true);
|
||||
const longListEl = $(`#savedTextsPopup .listLong`).empty();
|
||||
let longList = "";
|
||||
if (longNames.length === 0) {
|
||||
longList += "<div>No saved long custom texts found</div>";
|
||||
} else {
|
||||
for (const name of longNames) {
|
||||
longList += `<div class="savedText">
|
||||
<div class="button name">${escapeHTML(name)}</div>
|
||||
<div class="button ${
|
||||
CustomText.getCustomTextLongProgress(name) <= 0 ? "disabled" : ""
|
||||
} resetProgress">reset</div>
|
||||
<div class="button delete">
|
||||
<i class="fas fa-fw fa-trash"></i>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
longListEl.html(longList);
|
||||
}
|
||||
|
||||
let callbackFuncOnHide: (() => void) | undefined = undefined;
|
||||
|
||||
export async function show(
|
||||
noAnim = false,
|
||||
callbackOnHide?: () => void
|
||||
): Promise<void> {
|
||||
Skeleton.append(wrapperId, "popups");
|
||||
if (!isPopupVisible(wrapperId)) {
|
||||
callbackFuncOnHide = callbackOnHide;
|
||||
fill();
|
||||
$("#savedTextsPopupWrapper")
|
||||
.stop(true, true)
|
||||
.css("opacity", 0)
|
||||
.removeClass("hidden")
|
||||
.animate({ opacity: 1 }, noAnim ? 0 : 125);
|
||||
}
|
||||
}
|
||||
|
||||
function hide(noAnim = false, noCallback = false): void {
|
||||
if (isPopupVisible(wrapperId)) {
|
||||
$("#savedTextsPopupWrapper")
|
||||
.stop(true, true)
|
||||
.css("opacity", 1)
|
||||
.animate(
|
||||
{
|
||||
opacity: 0,
|
||||
},
|
||||
noAnim ? 0 : 125,
|
||||
() => {
|
||||
$("#savedTextsPopupWrapper").addClass("hidden");
|
||||
Skeleton.remove(wrapperId);
|
||||
if (callbackFuncOnHide && !noCallback) callbackFuncOnHide();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function applySaved(name: string, long: boolean): void {
|
||||
let text = CustomText.getCustomText(name, long);
|
||||
if (long) {
|
||||
text = text.slice(CustomText.getCustomTextLongProgress(name));
|
||||
}
|
||||
CustomText.setPopupTextareaState(text.join(" "));
|
||||
}
|
||||
|
||||
$("#popups").on(
|
||||
"click",
|
||||
`#savedTextsPopupWrapper .list .savedText .button.name`,
|
||||
(e) => {
|
||||
const name = $(e.target).text();
|
||||
CustomTextState.setCustomTextName(name, false);
|
||||
applySaved(name, false);
|
||||
hide(true);
|
||||
}
|
||||
);
|
||||
|
||||
$("#popups").on(
|
||||
"click",
|
||||
`#savedTextsPopupWrapper .list .savedText .button.delete`,
|
||||
() => {
|
||||
hide(true, true);
|
||||
}
|
||||
);
|
||||
|
||||
$("#popups").on(
|
||||
"click",
|
||||
`#savedTextsPopupWrapper .listLong .savedText .button.name`,
|
||||
(e) => {
|
||||
const name = $(e.target).text();
|
||||
CustomTextState.setCustomTextName(name, true);
|
||||
applySaved(name, true);
|
||||
hide(true);
|
||||
}
|
||||
);
|
||||
|
||||
$("#popups").on(
|
||||
"click",
|
||||
`#savedTextsPopupWrapper .listLong .savedText .button.resetProgress`,
|
||||
() => {
|
||||
hide(true, true);
|
||||
}
|
||||
);
|
||||
|
||||
$("#popups").on(
|
||||
"click",
|
||||
`#savedTextsPopupWrapper .listLong .savedText .button.delete`,
|
||||
() => {
|
||||
hide(true, true);
|
||||
}
|
||||
);
|
||||
|
||||
$("#savedTextsPopupWrapper").on("mousedown", (e) => {
|
||||
if ($(e.target).attr("id") === "savedTextsPopupWrapper") {
|
||||
hide(true);
|
||||
}
|
||||
});
|
||||
|
||||
Skeleton.save(wrapperId);
|
||||
|
|
@ -16,19 +16,6 @@ export let word = -1;
|
|||
export let time = -1;
|
||||
export let section = -1;
|
||||
export let delimiter = " ";
|
||||
export let popupTextareaState = text.join(" ");
|
||||
export let savedPopupTextareaState = popupTextareaState;
|
||||
|
||||
export function setPopupTextareaStateToSaved(): void {
|
||||
popupTextareaState = savedPopupTextareaState;
|
||||
}
|
||||
|
||||
export function setPopupTextareaState(value: string, save = false): void {
|
||||
popupTextareaState = value;
|
||||
if (save) {
|
||||
savedPopupTextareaState = value;
|
||||
}
|
||||
}
|
||||
|
||||
export function setText(txt: string[]): void {
|
||||
text = txt;
|
||||
|
|
|
|||
|
|
@ -116,10 +116,6 @@ export function init(missed: boolean, slow: boolean): boolean {
|
|||
}
|
||||
|
||||
UpdateConfig.setMode("custom", true);
|
||||
CustomText.setPopupTextareaState(
|
||||
newCustomText.join(CustomText.delimiter),
|
||||
true
|
||||
);
|
||||
CustomText.setText(newCustomText);
|
||||
CustomText.setIsWordRandom(true);
|
||||
CustomText.setIsTimeRandom(false);
|
||||
|
|
|
|||
|
|
@ -246,9 +246,6 @@ export function restart(options = {} as RestartOptions): void {
|
|||
CustomText.setIsWordRandom(PractiseWords.before.customText.isWordRandom);
|
||||
CustomText.setWord(PractiseWords.before.customText.word);
|
||||
CustomText.setTime(PractiseWords.before.customText.time);
|
||||
CustomText.setPopupTextareaState(
|
||||
PractiseWords.before.customText.text.join(CustomText.delimiter)
|
||||
);
|
||||
}
|
||||
|
||||
UpdateConfig.setMode(PractiseWords.before.mode);
|
||||
|
|
@ -1067,16 +1064,11 @@ export async function finish(difficultyFailed = false): Promise<void> {
|
|||
|
||||
let newText = CustomText.getCustomText(customTextName, true);
|
||||
newText = newText.slice(newProgress);
|
||||
CustomText.setPopupTextareaState(
|
||||
newText.join(CustomText.delimiter),
|
||||
true
|
||||
);
|
||||
CustomText.setText(newText);
|
||||
} else {
|
||||
// They finished the test
|
||||
CustomText.setCustomTextLongProgress(customTextName, 0);
|
||||
const text = CustomText.getCustomText(customTextName, true);
|
||||
CustomText.setPopupTextareaState(text.join(CustomText.delimiter), true);
|
||||
CustomText.setText(text);
|
||||
Notifications.add("Long custom text completed", 1, {
|
||||
duration: 5,
|
||||
|
|
|
|||
|
|
@ -18,29 +18,33 @@ type ConstructorCustomAnimations = {
|
|||
hide?: CustomWrapperAndModalAnimations;
|
||||
};
|
||||
|
||||
type ShowHideOptions = {
|
||||
type Animation<T> = (modal: HTMLElement, modalChainData?: T) => Promise<void>;
|
||||
|
||||
type ShowHideOptions<T> = {
|
||||
animationMode?: "none" | "both" | "modalOnly";
|
||||
animationDurationMs?: number;
|
||||
customAnimation?: CustomWrapperAndModalAnimations;
|
||||
beforeAnimation?: (modal: HTMLElement) => Promise<void>;
|
||||
afterAnimation?: (modal: HTMLElement) => Promise<void>;
|
||||
beforeAnimation?: Animation<T>;
|
||||
afterAnimation?: Animation<T>;
|
||||
modalChainData?: T;
|
||||
};
|
||||
|
||||
export type ShowOptions = ShowHideOptions & {
|
||||
export type ShowOptions<T = unknown> = ShowHideOptions<T> & {
|
||||
mode?: "modal" | "dialog";
|
||||
focusFirstInput?: true | "focusAndSelect";
|
||||
modalChain?: AnimatedModal;
|
||||
};
|
||||
|
||||
export type HideOptions = ShowHideOptions & {
|
||||
export type HideOptions<T = unknown> = ShowHideOptions<T> & {
|
||||
clearModalChain?: boolean;
|
||||
dontShowPreviousModalInchain?: boolean;
|
||||
};
|
||||
|
||||
type ConstructorParams = {
|
||||
type ConstructorParams<T> = {
|
||||
dialogId: string;
|
||||
appendTo?: Skeleton.SkeletonAppendParents;
|
||||
customAnimations?: ConstructorCustomAnimations;
|
||||
showOptionsWhenInChain?: ShowOptions;
|
||||
showOptionsWhenInChain?: ShowOptions<T>;
|
||||
customEscapeHandler?: (e: KeyboardEvent) => void;
|
||||
customWrapperClickHandler?: (e: MouseEvent) => void;
|
||||
setup?: (modal: HTMLElement) => Promise<void>;
|
||||
|
|
@ -49,14 +53,19 @@ type ConstructorParams = {
|
|||
const DEFAULT_ANIMATION_DURATION = 125;
|
||||
const MODAL_ONLY_ANIMATION_MULTIPLIER = 0.75;
|
||||
|
||||
export default class AnimatedModal {
|
||||
export default class AnimatedModal<
|
||||
IncomingModalChainData = unknown,
|
||||
OutgoingModalChainData = unknown
|
||||
> {
|
||||
private wrapperEl: HTMLDialogElement;
|
||||
private modalEl: HTMLElement;
|
||||
private dialogId: string;
|
||||
private open = false;
|
||||
private setupRan = false;
|
||||
private previousModalInChain: AnimatedModal | undefined;
|
||||
private showOptionsWhenInChain: ShowOptions | undefined;
|
||||
private showOptionsWhenInChain:
|
||||
| ShowOptions<IncomingModalChainData>
|
||||
| undefined;
|
||||
private skeletonAppendParent: Skeleton.SkeletonAppendParents;
|
||||
private customShowAnimations: CustomWrapperAndModalAnimations | undefined;
|
||||
private customHideAnimations: CustomWrapperAndModalAnimations | undefined;
|
||||
|
|
@ -65,7 +74,7 @@ export default class AnimatedModal {
|
|||
private customWrapperClickHandler: ((e: MouseEvent) => void) | undefined;
|
||||
private setup: ((modal: HTMLElement) => Promise<void>) | undefined;
|
||||
|
||||
constructor(constructorParams: ConstructorParams) {
|
||||
constructor(constructorParams: ConstructorParams<IncomingModalChainData>) {
|
||||
if (constructorParams.dialogId.startsWith("#")) {
|
||||
constructorParams.dialogId = constructorParams.dialogId.slice(1);
|
||||
}
|
||||
|
|
@ -183,7 +192,7 @@ export default class AnimatedModal {
|
|||
}
|
||||
}
|
||||
|
||||
async show(options?: ShowOptions): Promise<void> {
|
||||
async show(options?: ShowOptions<IncomingModalChainData>): Promise<void> {
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
return new Promise(async (resolve) => {
|
||||
if (this.open) return resolve();
|
||||
|
|
@ -210,6 +219,8 @@ export default class AnimatedModal {
|
|||
await this.previousModalInChain.hide({
|
||||
animationMode: "modalOnly",
|
||||
animationDurationMs: modalAnimationDuration,
|
||||
dontShowPreviousModalInchain:
|
||||
options.modalChain.previousModalInChain !== undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -220,7 +231,7 @@ export default class AnimatedModal {
|
|||
this.wrapperEl.showModal();
|
||||
}
|
||||
|
||||
await options?.beforeAnimation?.(this.modalEl);
|
||||
await options?.beforeAnimation?.(this.modalEl, options?.modalChainData);
|
||||
|
||||
//wait until the next event loop to allow the dialog to start animating
|
||||
setTimeout(async () => {
|
||||
|
|
@ -270,7 +281,10 @@ export default class AnimatedModal {
|
|||
wrapperAnimation.easing ?? "swing",
|
||||
async () => {
|
||||
this.focusFirstInput(options?.focusFirstInput);
|
||||
await options?.afterAnimation?.(this.modalEl);
|
||||
await options?.afterAnimation?.(
|
||||
this.modalEl,
|
||||
options?.modalChainData
|
||||
);
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
|
|
@ -288,7 +302,10 @@ export default class AnimatedModal {
|
|||
modalAnimation?.easing ?? "swing",
|
||||
async () => {
|
||||
this.focusFirstInput(options?.focusFirstInput);
|
||||
await options?.afterAnimation?.(this.modalEl);
|
||||
await options?.afterAnimation?.(
|
||||
this.modalEl,
|
||||
options?.modalChainData
|
||||
);
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
|
|
@ -296,7 +313,7 @@ export default class AnimatedModal {
|
|||
});
|
||||
}
|
||||
|
||||
async hide(options?: HideOptions): Promise<void> {
|
||||
async hide(options?: HideOptions<OutgoingModalChainData>): Promise<void> {
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
return new Promise(async (resolve) => {
|
||||
if (!isPopupVisible(this.dialogId)) return resolve();
|
||||
|
|
@ -361,9 +378,13 @@ export default class AnimatedModal {
|
|||
this.open = false;
|
||||
await options?.afterAnimation?.(this.modalEl);
|
||||
|
||||
if (this.previousModalInChain !== undefined) {
|
||||
if (
|
||||
this.previousModalInChain !== undefined &&
|
||||
!options?.dontShowPreviousModalInchain
|
||||
) {
|
||||
await this.previousModalInChain.show({
|
||||
animationMode: "modalOnly",
|
||||
modalChainData: options?.modalChainData,
|
||||
animationDurationMs:
|
||||
modalAnimationDuration * MODAL_ONLY_ANIMATION_MULTIPLIER,
|
||||
...this.previousModalInChain.showOptionsWhenInChain,
|
||||
|
|
@ -393,9 +414,13 @@ export default class AnimatedModal {
|
|||
this.open = false;
|
||||
await options?.afterAnimation?.(this.modalEl);
|
||||
|
||||
if (this.previousModalInChain !== undefined) {
|
||||
if (
|
||||
this.previousModalInChain !== undefined &&
|
||||
!options?.dontShowPreviousModalInchain
|
||||
) {
|
||||
await this.previousModalInChain.show({
|
||||
animationMode: "modalOnly",
|
||||
modalChainData: options?.modalChainData,
|
||||
animationDurationMs:
|
||||
modalAnimationDuration * MODAL_ONLY_ANIMATION_MULTIPLIER,
|
||||
...this.previousModalInChain.showOptionsWhenInChain,
|
||||
|
|
|
|||
|
|
@ -146,10 +146,6 @@ export function loadTestSettingsFromUrl(getOverride?: string): void {
|
|||
|
||||
if (de[2] !== null) {
|
||||
const customTextSettings = de[2];
|
||||
CustomText.setPopupTextareaState(
|
||||
customTextSettings.text.join(customTextSettings.delimiter),
|
||||
true
|
||||
);
|
||||
CustomText.setText(customTextSettings.text);
|
||||
CustomText.setIsTimeRandom(customTextSettings.isTimeRandom);
|
||||
CustomText.setIsWordRandom(customTextSettings.isWordRandom);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue