// ==UserScript== // @name USPS Address Validation - Add/Edit Page // @namespace https://github.com/nate-kean/ // @version 2026.01.08.2 // @description Integrate USPS address validation and autofill into the Address fields. // @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 // @require https://update.greasyfork.icu/scripts/555040/1730304/USPS%20Address%20Validation%20-%20Common.js // @downloadURL https://update.greasyfork.icu/scripts/557826/USPS%20Address%20Validation%20-%20AddEdit%20Page.user.js // @updateURL https://update.greasyfork.icu/scripts/557826/USPS%20Address%20Validation%20-%20AddEdit%20Page.meta.js // ==/UserScript== // @ts-check /** * Entry point for the program. * Holds the Add/Edit-page-specific logic for capturing addresses. */ /** * @typedef {Object} Fields * @property {HTMLTextAreaElement} streetAddress * @property {HTMLInputElement} city * @property {HTMLInputElement} state * @property {HTMLInputElement} zip * @property {HTMLInputElement} country */ class AddEditPageController { static #TYPING_WAIT_TIME_MS = 1_000; // ASCII letters or the delete keys static #ALLOWED_KEYS = /^([\x20-\x7E])|(Backspace)|(Delete)$/; /** @type {(keyof Fields)[]} */ static #REQUIRED_FIELD_NAMES = ["streetAddress", "city", "state"]; #panel; #heading; #validator; #indicator; /** @type {ReturnType | undefined} */ #timeout = undefined; /** @type {Fields} */ #fields; /** @type {HTMLSelectElement} */ #flagSelect; /** @type {string[]} */ #streetAddressLines = []; /** * @param {string} id * @param {string} panelSelector * @param {string} headingSelector */ constructor(id, panelSelector, headingSelector) { this.#panel = tryQuerySelector(document, panelSelector); this.#heading = tryQuerySelector(document, headingSelector); this.#validator = new Validator(); this.#indicator = new Indicator(this.#heading); this.#flagSelect = this.#getFlagSelect(); // "Starts with" selectors because the IDs are all "id" and "id2" // between the two address panels this.#fields = { // Want to pass these as type parameters but casting is the best I // can do without converting this whole thing to .ts streetAddress: /** @type {HTMLTextAreaElement} */ (tryQuerySelector(this.#panel, 'textarea[id^="address"')), city: /** @type {HTMLInputElement} */ (tryQuerySelector(this.#panel, 'input[id^="city"')), state: /** @type {HTMLInputElement} */ (tryQuerySelector(this.#panel, 'input[id^="state"')), zip: /** @type {HTMLInputElement} */ (tryQuerySelector(this.#panel, 'input[id^="zipcode"')), country: /** @type {HTMLInputElement} */ (tryQuerySelector(this.#panel, 'input[id^="country"')), }; // Fill the address if the indicator is clicked this.#indicator.button.addEventListener( "click", this.#fillPanel.bind(this), { passive: true }, ); // Listen for keyboard input in this controller's address panel // @ts-ignore -- My TypeScript doesn't know keyup is a KeyboardEvent??? this.#panel.addEventListener( "keyup", this.#onKeypress.bind(this), { passive: true }, ); // Re-process the address if the Address Validation field changes // This