aesy/reddit-comment-highlights

View on GitHub
src/options/OptionsPage.ts

Summary

Maintainability
C
7 hrs
Test Coverage
import "options/module.scss";
import { Options } from "options/ExtensionOptions"
import { Actions } from "common/Actions";
import { extensionFunctionRegistry } from "common/Registries";

const element = {
    content: document.querySelector(".main-content")!,
    CSSTextArea: document.getElementById("CSS-text") as HTMLInputElement,
    CSSClassNameInput: document.getElementById("class-name") as HTMLInputElement,
    CSSClassName: document.querySelector(".class-name")!,
    borderInput: document.getElementById("border") as HTMLInputElement,
    clearCommentInput: document.getElementById("clear-comment") as HTMLInputElement,
    clearChildrenInput: document.getElementById("clear-child-comments") as HTMLInputElement,
    frequencyInput: document.getElementById("frequency") as HTMLInputElement,
    frequencyNumber: document.getElementById("frequency-number")!,
    frequencyUnit: document.getElementById("frequency-unit")!,
    colorPickerRadioButton: document.getElementById("color-pickers")!,
    customCSSRadioButton: document.getElementById("custom-css") as HTMLInputElement,
    statusMessage: document.getElementById("status-message")!,
    styleModes: document.querySelectorAll("input[name=\"style-mode\"]")!,
    advancedDialog: document.getElementById("advanced")!,
    advancedSettings: document.querySelectorAll(".advanced")!,
    advancedButton: document.getElementById("show-advanced")!,
    saveButton: document.getElementById("save-options")!,
    tabs: document.querySelectorAll(".main-content__tab")!,
    resetButton: document.getElementById("clear-all")!,
    year: document.getElementById("footer__year")!,
    compressedStorage: document.getElementById("compressed-storage") as HTMLInputElement,
    syncedStorage: document.getElementById("sync-storage") as HTMLInputElement,
    debug: document.getElementById("debug-mode") as HTMLInputElement,
    sendErrors: document.getElementById("send-errors") as HTMLInputElement,
    backgroundColorPicker: document.getElementById("back-color") as HTMLInputElement,
    backgroundNightColorPicker: document.getElementById("back-color-night") as HTMLInputElement,
    textColorPicker: document.getElementById("front-color") as HTMLInputElement,
    textNightColorPicker: document.getElementById("front-color-night") as HTMLInputElement,
    customLinkColor: document.getElementById("custom-link-color") as HTMLInputElement,
    linkColorPicker: document.getElementById("link-color") as HTMLInputElement,
    linkNightColorPicker: document.getElementById("link-color-night") as HTMLInputElement,
    customQuoteColor: document.getElementById("custom-quote-color") as HTMLInputElement,
    quoteTextColorPicker: document.getElementById("quote-text-color") as HTMLInputElement,
    quoteTextNightColorPicker: document.getElementById("quote-text-color-night") as HTMLInputElement
};

const state = {
    showAdvancedSettings: false
};

class Message {
    private static timeOutId: NodeJS.Timeout;

    public static show(text: string, isError = false): void {
        element.statusMessage.textContent = text;
        element.statusMessage.classList.add("status-message--is-visible");

        if (Message.timeOutId) {
            clearTimeout(Message.timeOutId);
        }

        element.statusMessage.classList.toggle("status-message--success", !isError);
        element.statusMessage.classList.toggle("status-message--error", isError);

        Message.timeOutId = setTimeout(() => {
            element.statusMessage.classList.remove("status-message--is-visible");
        }, 3000);
    }
}

function isValidCSSClassName(className: string): boolean {
    return /^[a-z_]|-[a-z_-][a-z\d_-]*$/i.test(className);
}

async function save(): Promise<void> {
    const options: Partial<Options> = {
        backColor: element.backgroundColorPicker.value,
        backNightColor: element.backgroundNightColorPicker.value,
        frontColor: element.textColorPicker.value,
        frontNightColor: element.textNightColorPicker.value,
        linkColor: element.customLinkColor.checked ? element.linkColorPicker.value : null,
        linkNightColor: element.customLinkColor.checked ? element.linkNightColorPicker.value : null,
        quoteTextColor: element.customQuoteColor.checked ? element.quoteTextColorPicker.value : null,
        quoteTextNightColor: element.customQuoteColor.checked ? element.quoteTextNightColorPicker.value : null,
        threadRemovalTimeSeconds: parseInt(element.frequencyInput.value) * 86400,
        border: element.borderInput.checked ? undefined : null, // undefined = default, null = none
        clearCommentOnClick: element.clearCommentInput.checked,
        clearCommentincludeChildren: element.clearChildrenInput.checked,
        customCSS: element.customCSSRadioButton.checked ? element.CSSTextArea.value : null,
        customCSSClassName: element.customCSSRadioButton.checked ? element.CSSClassNameInput.value : undefined,
        useCompression: element.compressedStorage.checked,
        sync: element.syncedStorage.checked,
        debug: element.debug.checked,
        sendErrorReports: element.sendErrors.checked
    };

    try {
        await extensionFunctionRegistry.invoke(Actions.SAVE_OPTIONS, options);
    } catch (error) {
        Message.show("Save unsuccessful (see console for detailed error message)", true);
        return console.warn(error);
    }

    Message.show("Settings saved!", false);
}

async function update(): Promise<void> {
    const className = element.CSSClassNameInput.value.trim();
    const valid = isValidCSSClassName(className);

    element.CSSClassNameInput.classList.toggle("text-input--invalid", !valid);
    element.CSSClassNameInput.textContent = className;
    element.CSSClassName.textContent = className;
    element.clearChildrenInput.disabled = !element.clearCommentInput.checked;
    element.linkColorPicker.disabled = !element.customLinkColor.checked;
    element.linkNightColorPicker.disabled = !element.customLinkColor.checked;
    element.quoteTextColorPicker.disabled = !element.customQuoteColor.checked;
    element.quoteTextNightColorPicker.disabled = !element.customQuoteColor.checked;

    const selection = document.querySelector("input[name=\"style-mode\"]:checked")!;

    for (const tab of element.tabs) {
        tab.classList.toggle(
            "main-content__tab--is-visible",
            tab.classList.contains(selection.id)
        );
    }

    const frequencyValue = element.frequencyInput.value;

    switch (parseInt(frequencyValue, 10)) {
        case 1:
            element.frequencyNumber.textContent = String(1);
            element.frequencyUnit.textContent = "day";
            break;
        case 7:
            element.frequencyNumber.textContent = String(1);
            element.frequencyUnit.textContent = "week";
            break;
        case 14:
            element.frequencyNumber.textContent = String(2);
            element.frequencyUnit.textContent = "weeks";
            break;
        default:
            element.frequencyNumber.textContent = frequencyValue;
            element.frequencyUnit.textContent = "days";
    }

    element.advancedDialog.classList.toggle(
        "options-section--hidden",
        state.showAdvancedSettings
    );

    for (const setting of element.advancedSettings) {
        setting.classList.toggle("options-section--hidden", !state.showAdvancedSettings);
    }

    element.content.classList.remove("main-content--hidden");
}

async function load(): Promise<void> {
    let options: Options;

    try {
        options = await extensionFunctionRegistry.invoke<void, Options>(Actions.GET_OPTIONS);
    } catch (error) {
        Message.show("Load unsuccessful (see console for detailed error message)", true);
        return console.warn(error);
    }

    element.backgroundColorPicker.value = options.backColor;
    element.backgroundNightColorPicker.value = options.backNightColor;
    element.textColorPicker.value = options.frontColor;
    element.textNightColorPicker.value = options.frontNightColor;
    element.frequencyInput.value = String(options.threadRemovalTimeSeconds / 86400);
    element.borderInput.checked = Boolean(options.border);
    element.clearCommentInput.checked = options.clearCommentOnClick;
    element.clearChildrenInput.checked = options.clearCommentincludeChildren;
    element.customLinkColor.checked = Boolean(options.linkColor);
    element.customQuoteColor.checked = Boolean(options.quoteTextColor);
    element.compressedStorage.checked = options.useCompression;
    element.syncedStorage.checked = options.sync;
    element.debug.checked = options.debug;
    element.sendErrors.checked = options.sendErrorReports;

    if (options.linkColor) {
        element.linkColorPicker.value = options.linkColor;
    }

    if (options.linkNightColor) {
        element.linkNightColorPicker.value = options.linkNightColor;
    }

    if (options.quoteTextColor) {
        element.quoteTextColorPicker.value = options.quoteTextColor;
    }

    if (options.quoteTextNightColor) {
        element.quoteTextNightColorPicker.value = options.quoteTextNightColor;
    }

    if (options.customCSS) {
        element.customCSSRadioButton.click();
        element.CSSTextArea.value = options.customCSS;
    } else {
        element.colorPickerRadioButton.click();
    }

    if (options.customCSSClassName) {
        element.CSSClassNameInput.value = options.customCSSClassName;
    }

    element.year.textContent = String(new Date().getFullYear());

    await update();
}

async function reset(): Promise<void> {
    state.showAdvancedSettings = false;

    scrollTo({
        top: 0,
        left: 0,
        behavior: "smooth"
    });

    try {
        await extensionFunctionRegistry.invoke(Actions.CLEAR_STORAGE, undefined);
    } catch (error) {
        Message.show("Reset unsuccessful (see console for detailed error message)", true);
        return console.warn(error);
    }

    Message.show("Settings reset!", false);
    await load();
}

async function initializeListeners(): Promise<void> {
    element.saveButton.addEventListener("click", save);
    element.resetButton.addEventListener("click", reset);
    element.advancedButton.addEventListener("click", async () => {
        state.showAdvancedSettings = true;
        await update();
    });
    element.clearCommentInput.addEventListener("click", update);
    element.customLinkColor.addEventListener("click", update);
    element.customQuoteColor.addEventListener("click", update);
    element.frequencyInput.addEventListener("input", update);
    element.CSSClassNameInput.addEventListener("input", async () => {
        const selection = {
            start: element.CSSClassNameInput.selectionStart!,
            end: element.CSSClassNameInput.selectionEnd!
        };

        // This loses the selection of the input for some reason
        await update();

        // Restore selection (has to be async)
        setTimeout(() => {
            element.CSSClassNameInput.setSelectionRange(selection.start, selection.end);
        }, 0);
    });

    for (const styleMode of element.styleModes) {
        styleMode.addEventListener("click", update);
    }
}

async function initialize(): Promise<void> {
    await Promise.all([
        initializeListeners(),
        load()
    ]);
}

document.addEventListener("DOMContentLoaded", async () => {
    try {
        await initialize();
    } catch (error) {
        Message.show("An error occurred (see console for detailed error message)", true);
        console.warn(error);
    }
});