BareGit
/** Return the non-executable form configuration emitted by the server. */
function getCardPageData()
{
    return JSON.parse(document.getElementById("CardPageData").textContent);
}

/** Return whether the current form is editing an existing card. */
function isEditForm()
{
    return getCardPageData().mode == "edit";
}

/** Return whether the form requests replacement front artwork. */
function replacesFront()
{
    if(!isEditForm())
    {
        return true;
    }
    return document.getElementById("CardFrontAction").value == "replace";
}

/** Return whether the form requests replacement foil control. */
function replacesFoil()
{
    if(!isEditForm())
    {
        return true;
    }
    return document.getElementById("CardFoilAction").value == "replace";
}

/** Show and enable image-source fields required by the current actions. */
function updateSourceMode()
{
    const selected_mode = document.querySelector(
        'input[name="source_mode"]:checked').value;
    const uses_files = selected_mode == "files";
    const file_fields = document.getElementById("FileSourceFields");
    const url_fields = document.getElementById("UrlSourceFields");
    file_fields.hidden = !uses_files;
    url_fields.hidden = uses_files;

    const front_file = document.getElementById("CardFrontFile");
    const foil_file = document.getElementById("CardFoilFile");
    const front_url = document.getElementById("CardFrontUrl");
    const foil_url = document.getElementById("CardFoilUrl");
    const state = window.cardFormLogic.sourceInputState({
        mode: isEditForm() ? "edit" : "create",
        source_mode: selected_mode,
        front_action: replacesFront() ? "replace" : "keep",
        foil_action: replacesFoil() ? "replace" : "keep",
    });
    front_file.disabled = !state.front_file_enabled;
    foil_file.disabled = !state.foil_file_enabled;
    front_url.disabled = !state.front_url_enabled;
    foil_url.disabled = !state.foil_url_enabled;
    front_file.required = state.front_file_enabled && state.front_required;
    front_url.required = state.front_url_enabled && state.front_required;
    foil_file.required = state.foil_file_enabled && state.foil_required;
    foil_url.required = state.foil_url_enabled && state.foil_required;
}

/** Show controls and series belonging to the selected database game. */
function updateSeriesChoices()
{
    const game = document.getElementById("CardGame").value;
    for(const group of document.querySelectorAll(".game-fields"))
    {
        const visible = window.cardFormLogic.gameControlEnabled(
            game, group.dataset.game);
        group.hidden = !visible;
        for(const input of group.querySelectorAll("input, textarea, select"))
        {
            input.disabled = !visible;
        }
    }
    for(const choice of document.querySelectorAll(".series-choice"))
    {
        const visible = window.cardFormLogic.seriesChoiceEnabled(
            game, choice.dataset.game);
        choice.hidden = !visible;
        choice.querySelector("input").disabled = !visible;
    }
}

/** Display the selected image filename beside one file input. */
function updateFileMetadata(input, output)
{
    const file = input.files[0];
    output.textContent = file == null ? "No file selected" : file.name;
}

/** Put one browser Blob into a file input for multipart submission. */
function setInputFile(input, blob, filename)
{
    const transfer = new DataTransfer();
    transfer.items.add(new File([blob], filename, {type: blob.type}));
    input.files = transfer.files;
}

/** Fetch an existing same-origin image for a mixed edit preview. */
async function fetchExistingImage(url, filename)
{
    const response = await fetch(url, {credentials: "same-origin"});
    if(!response.ok)
    {
        throw(new Error(
            `Failed to load the current card image: HTTP ${response.status}`));
    }
    const blob = await response.blob();
    return new File([blob], filename, {type: blob.type});
}

/** Resolve retained and replacement files for the current preview. */
async function getPreviewFiles()
{
    const page_data = getCardPageData();
    const selected_front = document.getElementById(
        "CardFrontFile").files[0];
    const selected_foil = document.getElementById(
        "CardFoilFile").files[0];
    if(page_data.mode == "create")
    {
        return {front: selected_front, foil: selected_foil};
    }

    let front = selected_front;
    if(!replacesFront() || front == null)
    {
        front = await fetchExistingImage(
            page_data.front_url, "current-front");
    }
    const foil_action = document.getElementById("CardFoilAction").value;
    let foil = selected_foil;
    if(foil_action == "remove")
    {
        foil = null;
    }
    else if((foil_action == "keep" || foil == null) &&
            page_data.foil_url != null)
    {
        foil = await fetchExistingImage(
            page_data.foil_url, "current-foil");
    }
    return {front, foil};
}

/** Refresh the WebGL preview from retained and selected image files. */
async function updateLocalPreview()
{
    const preview = window.cardPreview;
    if(preview == null)
    {
        return;
    }
    const files = await getPreviewFiles();
    if(files.front != null)
    {
        await preview.setFiles(files.front, files.foil);
    }
}

/** Fetch one CORS-enabled URL into its corresponding file input. */
async function fetchImageInput(url_input, file_input, filename)
{
    if(!window.cardFormLogic.validRemoteImageUrl(url_input.value))
    {
        throw(new Error("Image URLs must use HTTP or HTTPS."));
    }
    const url = new URL(url_input.value);
    const response = await fetch(url, {
        mode: "cors",
        credentials: "omit",
    });
    if(!response.ok)
    {
        throw(new Error(
            `Failed to fetch ${url}: HTTP ${response.status}`));
    }
    setInputFile(file_input, await response.blob(), filename);
}

/** Prepare URL images and the foil thumbnail before native submission. */
async function prepareCardSubmission(event)
{
    const form = event.currentTarget;
    if(form.dataset.prepared == "true")
    {
        return;
    }
    event.preventDefault();
    if(form.dataset.state == "preparing")
    {
        return;
    }
    form.dataset.state = "preparing";
    const submitter = event.submitter;
    const submit_button = form.querySelector('button[type="submit"]');
    submit_button.disabled = true;
    const status = document.getElementById("CardFormStatus");
    status.textContent = "Preparing card images…";

    try
    {
        const page_data = getCardPageData();
        const foil_action = page_data.mode == "edit"
            ? document.getElementById("CardFoilAction").value
            : "replace";
        const rendering_changed = page_data.mode == "create" ||
            replacesFront() || foil_action != "keep";
        const mode = document.querySelector(
            'input[name="source_mode"]:checked').value;
        const front_input = document.getElementById("CardFrontFile");
        const foil_input = document.getElementById("CardFoilFile");
        if(mode == "urls")
        {
            if(replacesFront())
            {
                await fetchImageInput(
                    document.getElementById("CardFrontUrl"),
                    front_input,
                    "front-upload");
                front_input.disabled = false;
            }
            if(replacesFoil())
            {
                foil_input.value = "";
                const foil_url = document.getElementById("CardFoilUrl");
                if(foil_url.value != "")
                {
                    await fetchImageInput(
                        foil_url, foil_input, "foil-upload");
                }
                foil_input.disabled = false;
            }
        }

        if(rendering_changed)
        {
            await updateLocalPreview();
        }
        const needs_thumbnail = window.cardFormLogic.foilThumbnailRequired({
            mode: page_data.mode,
            front_action: page_data.mode == "edit"
                ? document.getElementById("CardFrontAction").value
                : "replace",
            foil_action,
            has_current_foil: page_data.foil_url != null,
            has_replacement_foil: foil_input.files.length != 0,
        });
        if(needs_thumbnail)
        {
            if(window.cardPreview == null)
            {
                throw(new Error("The foil preview is not ready."));
            }
            const thumbnail = await window.cardPreview.captureThumbnail();
            const thumbnail_input = document.getElementById(
                "CardThumbnailFile");
            setInputFile(thumbnail_input, thumbnail, "thumbnail.png");
            thumbnail_input.disabled = false;
        }
        form.dataset.prepared = "true";
        form.dataset.state = "ready";
        submit_button.disabled = false;
        if(submitter == null)
        {
            form.requestSubmit();
        }
        else
        {
            form.requestSubmit(submitter);
        }
    }
    catch(error)
    {
        console.error(error);
        status.textContent = error.message;
        form.dataset.state = "idle";
        submit_button.disabled = false;
    }
}

/** Connect the image-source controls after the form is available. */
function initializeCardForm()
{
    for(const input of document.querySelectorAll(
        'input[name="source_mode"]'))
    {
        input.addEventListener("change", updateSourceMode);
    }

    const front_input = document.getElementById("CardFrontFile");
    const foil_input = document.getElementById("CardFoilFile");
    front_input.addEventListener("change", function updateFrontMetadata()
    {
        updateFileMetadata(
            front_input,
            document.getElementById("CardFrontFileMeta"));
        updateLocalPreview().catch(console.error);
    });
    foil_input.addEventListener("change", function updateFoilMetadata()
    {
        updateFileMetadata(
            foil_input,
            document.getElementById("CardFoilFileMeta"));
        updateLocalPreview().catch(console.error);
    });
    if(isEditForm())
    {
        document.getElementById("CardFrontAction").addEventListener(
            "change", function updateFrontAction()
            {
                updateSourceMode();
                updateLocalPreview().catch(console.error);
            });
        document.getElementById("CardFoilAction").addEventListener(
            "change", function updateFoilAction()
            {
                updateSourceMode();
                updateLocalPreview().catch(console.error);
            });
    }
    else
    {
        document.getElementById("CardGame").addEventListener(
            "change", updateSeriesChoices);
        updateSeriesChoices();
    }
    document.getElementById("CardForm").addEventListener(
        "submit", prepareCardSubmission);
    updateSourceMode();
}

window.addEventListener("DOMContentLoaded", initializeCardForm);