All files / src/views crawl-modal.ts

100% Statements 131/131
100% Branches 35/35
100% Functions 7/7
100% Lines 131/131

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 1611x             26x 26x 26x 26x 13x 26x 4x 4x 26x 3x 3x 6x 6x   1x   1x 26x   26x 26x 26x 26x 26x   26x 26x 26x 26x   26x   26x 26x 26x 26x 26x   26x 26x 26x 26x 26x 26x 26x 26x   26x 26x 26x 26x 26x 26x 26x 26x 26x   26x 26x 26x   26x 26x 32x 32x 2x 2x 32x 30x 30x 30x 32x 26x   26x 26x   26x   26x 26x 26x 26x 26x 26x 26x   26x 26x 26x 26x 26x 26x 26x       26x   26x 46x 46x 46x 46x 46x 46x 29x 29x 29x 46x 26x 26x   26x 26x 26x 17x 17x 1x 1x 1x 16x   16x 16x 16x   17x 3x 3x 3x 17x 13x 13x 13x 13x 7x 7x 7x 7x 6x 6x 6x 6x   9x 9x 26x   26x 26x 26x 26x  
import { App, Modal, Notice } from "obsidian";
import type LilbeePlugin from "../main";
import { MESSAGES } from "../locales/en";
import { bindEscapeToClose, ensureUrlScheme } from "../utils";
 
type ParseResult = { value: number | null; error: string | null };
 
function parseOptionalCount(raw: string, opts: { allowZero: boolean }): ParseResult {
    const errMsg = opts.allowZero ? MESSAGES.ERROR_CRAWL_DEPTH_INVALID : MESSAGES.ERROR_CRAWL_MAX_PAGES_POSITIVE;
    const trimmed = raw.trim();
    if (trimmed === "") return { value: null, error: null };
    const n = Number(trimmed);
    if (!Number.isFinite(n) || !Number.isInteger(n) || n < 0) {
        return { value: null, error: errMsg };
    }
    if (n === 0 && !opts.allowZero) {
        return { value: null, error: errMsg };
    }
    return { value: n, error: null };
}
 
const asInput = (el: HTMLElement): HTMLInputElement => el as unknown as HTMLInputElement;
 
export class CrawlModal extends Modal {
    private plugin: LilbeePlugin;
 
    constructor(app: App, plugin: LilbeePlugin) {
        super(app);
        this.plugin = plugin;
        bindEscapeToClose(this);
    }
 
    onOpen(): void {
        const { contentEl } = this;
        contentEl.empty();
        contentEl.addClass("lilbee-crawl-modal");
 
        contentEl.createEl("h2", { text: MESSAGES.TITLE_CRAWL_WEB_PAGE });
 
        const urlInput = contentEl.createEl("input", {
            cls: "lilbee-crawl-url",
            placeholder: MESSAGES.PLACEHOLDER_URL,
            attr: { type: "text" },
        });
 
        const recursiveRow = contentEl.createDiv({ cls: "lilbee-crawl-recursive-row" });
        const recursiveLabel = recursiveRow.createEl("label", { cls: "lilbee-crawl-recursive" });
        const recursiveInput = recursiveLabel.createEl("input", {
            cls: "lilbee-crawl-recursive-input",
            attr: { type: "checkbox" },
        });
        asInput(recursiveInput).checked = false;
        recursiveLabel.createSpan({ text: MESSAGES.LABEL_CRAWL_RECURSIVE });
 
        const infoBtn = recursiveRow.createEl("button", {
            cls: "lilbee-crawl-info-btn",
            text: "i",
            attr: {
                type: "button",
                "aria-label": MESSAGES.LABEL_CRAWL_RECURSIVE_INFO,
                "aria-expanded": "false",
            },
        });
 
        const notice = contentEl.createDiv({ cls: "lilbee-crawl-notice" });
        notice.setAttribute("hidden", "hidden");
        notice.textContent = MESSAGES.NOTICE_CRAWL_RECURSIVE;
 
        let noticeOpen = false;
        const setNoticeOpen = (open: boolean): void => {
            noticeOpen = open;
            if (open) {
                notice.removeAttribute("hidden");
                infoBtn.setAttribute("aria-expanded", "true");
            } else {
                notice.setAttribute("hidden", "hidden");
                infoBtn.setAttribute("aria-expanded", "false");
            }
        };
        infoBtn.addEventListener("click", () => setNoticeOpen(!noticeOpen));
 
        const advanced = contentEl.createEl("details", { cls: "lilbee-crawl-advanced" });
        advanced.createEl("summary", { text: MESSAGES.LABEL_CRAWL_ADVANCED });
 
        const options = advanced.createDiv({ cls: "lilbee-crawl-options" });
 
        const depthLabel = options.createEl("label", { text: MESSAGES.LABEL_DEPTH });
        const depthInput = depthLabel.createEl("input", {
            cls: "lilbee-crawl-depth",
            placeholder: MESSAGES.HINT_CRAWL_BLANK_NO_LIMIT,
            attr: { type: "number", min: "0" },
        });
        asInput(depthInput).value = "";
 
        const maxLabel = options.createEl("label", { text: MESSAGES.LABEL_MAX_PAGES });
        const maxInput = maxLabel.createEl("input", {
            cls: "lilbee-crawl-max-pages",
            placeholder: MESSAGES.HINT_CRAWL_BLANK_NO_LIMIT,
            attr: { type: "number", min: "1" },
        });
        asInput(maxInput).value = "";
 
        // Error element lives OUTSIDE the Advanced disclosure so it's visible
        // even when the user collapses Advanced after typing bad input.
        const errorEl = contentEl.createEl("div", { cls: "lilbee-crawl-error" });
 
        const syncRecursiveState = (): void => {
            const recursive = asInput(recursiveInput).checked;
            asInput(depthInput).disabled = !recursive;
            asInput(maxInput).disabled = !recursive;
            advanced.style.display = recursive ? "" : "none";
            infoBtn.style.display = recursive ? "" : "none";
            if (!recursive) {
                errorEl.textContent = "";
                setNoticeOpen(false);
            }
        };
        recursiveInput.addEventListener("change", syncRecursiveState);
        syncRecursiveState();
 
        const actions = contentEl.createDiv({ cls: "lilbee-crawl-actions" });
        const crawlBtn = actions.createEl("button", { text: MESSAGES.BUTTON_CRAWL, cls: "mod-cta" });
        crawlBtn.addEventListener("click", () => {
            const raw = asInput(urlInput).value.trim();
            if (!raw) {
                new Notice(MESSAGES.NOTICE_ENTER_URL);
                return;
            }
            const url = ensureUrlScheme(raw);
 
            const recursive = asInput(recursiveInput).checked;
            let depth: number | null;
            let maxPages: number | null;
 
            if (!recursive) {
                depth = 0;
                maxPages = null;
                errorEl.textContent = "";
            } else {
                const depthRes = parseOptionalCount(asInput(depthInput).value, { allowZero: true });
                const maxRes = parseOptionalCount(asInput(maxInput).value, { allowZero: false });
                const err = maxRes.error ?? depthRes.error;
                if (err) {
                    errorEl.textContent = err;
                    advanced.open = true;
                    return;
                }
                errorEl.textContent = "";
                depth = depthRes.value;
                maxPages = maxRes.value;
            }
 
            this.plugin.runCrawl(url, depth, maxPages);
            this.close();
        });
 
        const cancelBtn = actions.createEl("button", { text: MESSAGES.BUTTON_CANCEL });
        cancelBtn.addEventListener("click", () => this.close());
    }
}