Add/Edit: Validate dates

Make invalid dates block saving instead of throwing away progress.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Add/Edit: Validate dates
// @namespace    https://github.com/nate-kean/
// @version      2026.1.29.2
// @description  Make invalid dates block saving instead of throwing away progress.
// @author       Nate Kean
// @match        https://jamesriver.fellowshiponego.com/members/add*
// @match        https://jamesriver.fellowshiponego.com/members/edit/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=fellowshiponego.com
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    document.head.insertAdjacentHTML("beforeend", `
        <style id="nates-date-validation-css-fixes">
            .input-holder,
            /* Victim of CSS specificity arms race */
            #memberEditForm #existingGroupsHolder .form-group.group-dates-holder {
                border: none !important;

                &:focus-within {
                    border: none !important;
                }
                & > input.form-control.hasDatepicker,
                & input#memberEditSearchGroups {
                    border-width: 1px;
                    border-style: solid;
                    border-color: #d8d8d8;
                    margin-left: 0;

                    &.dateYear {
                        padding-left: 0 !important;
                    }

                    /* Red border if invalid AND not empty */
                    /* (Empty should not count as invalid */
                    &:invalid:not(:placeholder-shown) {
                        border-color: #fd5b63 !important;
                    }
                    &:focus {
                        border: 2px solid #176bfb !important;
                    }
                }
            }

            input#memberEditSearchGroups {
                padding-left: 26px !important;
            }

            .group-dates-holder {
                & input.hasDatepicker {
                    width: 132px !important;
                }
                & i {
                    right: 23px !important;
                }
            }

            .date-holder .input-holder i {
                top: 36px;
            }


            /* Insanely specific selector from F1 Go that I have to make extra effort to compete with */
            #memberEditForm .group-dates .input-holder:focus-within input {
                border: 2px solid #176bfb !important;
            }
        </style>
    `);

    function patch(dateField) {
        dateField.pattern = "\\d{2}\\/\\d{2}\\/\\d{4}";
        dateField.addEventListener("keyup", () => {
            // Make invalid input fields temporarily required so that the user agent
            // prevents the form from being submitted until they're corrected
            dateField.required = dateField.value.length > 0 && !dateField.checkValidity();
        });
        dateField.placeholder = dateField.placeholder || " ";
    }

    // Patch the page's date fields
    const form = document.querySelector("form#memberEditForm");
    for (const dateField of document.querySelectorAll("input.hasDatepicker")) {
        patch(dateField);
    }

    // Patch jQuery to run requestSubmit() instead of submit() on the page's form
    // so that the validation checks get run
    const patchedForm = $(form);
    patchedForm.submit = () => {
        form.requestSubmit();
    };
    const jq = $;
    window.$ = new Proxy(jq, {
        apply(target, thisArg, args) {
            if (args[0] === "#memberEditForm") return patchedForm;
            return Reflect.apply(target, thisArg, args);
        },
        get(...args) {
            return Reflect.get(...args);
        },
    });

    // Patch new date fields as they're generated in the Add Groups section
    const observer = new MutationObserver(records => {
        for (const record of records) {
            for (const node of record.addedNodes) {
                const dateField = node.querySelector(".hasDatepicker");
                if (dateField === null) continue;
                patch(dateField);
            }
        }
    });
    observer.observe(document.querySelector("#memberEditGroupHolder"), {
        subtree: true,
        childList: true,
    });
})();