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:
Jack 2024-04-05 13:18:26 +02:00 committed by GitHub
parent 3cbf0bda4c
commit ff7816aac0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 826 additions and 885 deletions

View file

@ -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 -->

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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";

View file

@ -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);

View file

@ -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 (

View file

@ -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();
});

View 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,
},
});

View file

@ -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 {

View 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,
});

View 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();
},
},
});

View file

@ -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']",

View file

@ -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,
});

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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;

View file

@ -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);

View file

@ -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,

View file

@ -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,

View file

@ -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);