Created
January 25, 2026 21:41
-
-
Save vfontjr/095de80f21922b7a3570ef41a765e313 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| jQuery(document).ready(function ($) { | |
| "use strict"; | |
| /** | |
| * Particles and connectors that are commonly lowercase in surnames and multi-part names | |
| * (EXCEPT when they are the first word in the full string). | |
| * | |
| * Examples: | |
| * - "Font de Lahara" -> "de" stays lowercase | |
| * - "Juan de la Cruz" -> "de" and "la" stay lowercase | |
| */ | |
| const lowercaseWords = new Set([ | |
| "de", "del", "de la", "de las", "de los", // multi-word particles handled below | |
| "da", "di", "du", | |
| "la", "le", "lo", "las", "los", | |
| "van", "von", "der", "den", "ten", | |
| "bin", "ibn", | |
| "al", "el" | |
| ]); | |
| /** | |
| * Optional: common abbreviations / initials / suffixes you usually want uppercased. | |
| * You can extend this list as needed for your forms. | |
| */ | |
| const forceUppercaseTokens = new Set([ | |
| "ii", "iii", "iv", "vi", "vii", "viii", | |
| "jr", "sr", | |
| "md", "phd", "esq" | |
| ]); | |
| /** | |
| * Capitalize a token while respecting apostrophes and hyphens. | |
| * | |
| * Handles: | |
| * - Hyphenated: "smith-jones" -> "Smith-Jones" | |
| * - Apostrophes: "o'connor" -> "O'Connor" | |
| * - Mixed: "d'angelo-smith" -> "D'Angelo-Smith" | |
| * | |
| * Strategy: | |
| * 1) Lowercase the whole token | |
| * 2) Split by hyphen, capitalize each segment | |
| * 3) Within each segment, split by apostrophe, capitalize each part | |
| * 4) Re-join with the original punctuation | |
| */ | |
| function capitalizeToken(token) { | |
| if (!token) return ""; | |
| const lowerToken = token.toLowerCase(); | |
| // Split by hyphen but keep capitalization of each side | |
| const hyphenParts = lowerToken.split("-"); | |
| const cappedHyphenParts = hyphenParts.map((part) => { | |
| // Split by apostrophe | |
| const apostropheParts = part.split("'"); | |
| const cappedApostropheParts = apostropheParts.map((p) => { | |
| if (!p) return ""; | |
| return p.charAt(0).toUpperCase() + p.slice(1); | |
| }); | |
| return cappedApostropheParts.join("'"); | |
| }); | |
| return cappedHyphenParts.join("-"); | |
| } | |
| /** | |
| * Normalize whitespace: | |
| * - trims leading/trailing space | |
| * - collapses internal whitespace to single spaces | |
| */ | |
| function normalizeWhitespace(input) { | |
| return (input || "").trim().replace(/\s+/g, " "); | |
| } | |
| /** | |
| * Main formatter: "smart title case" for names. | |
| * | |
| * Features: | |
| * - Title-cases each word | |
| * - Keeps surname particles lowercased (de, la, van, etc.) when not first word | |
| * - Supports hyphenated and apostrophe names | |
| * - Optionally uppercases common suffixes/credentials (Jr, Sr, PhD, etc.) | |
| * - Supports multi-word particles like "de la", "de los", etc. | |
| */ | |
| function smartNameCase(input) { | |
| const normalized = normalizeWhitespace(input); | |
| if (!normalized) return ""; | |
| // Tokenize on spaces | |
| const words = normalized.split(" "); | |
| const output = []; | |
| for (let i = 0; i < words.length; i++) { | |
| const raw = words[i]; | |
| const lower = raw.toLowerCase(); | |
| /** | |
| * 1) Multi-word particle handling | |
| * We want "de la" to remain "de la" (both lowercase), etc. | |
| * | |
| * If we see "de" and the next word is "la/las/los", treat them as one phrase. | |
| */ | |
| const next = (words[i + 1] || "").toLowerCase(); | |
| const twoWordParticle = `${lower} ${next}`; | |
| if (i !== 0 && lowercaseWords.has(twoWordParticle)) { | |
| // Push both words lowercased as-is | |
| output.push(lower); | |
| output.push(next); | |
| i++; // skip the next word because we already consumed it | |
| continue; | |
| } | |
| /** | |
| * 2) Single-word particle handling | |
| * Keep it lowercase unless it is the first word. | |
| */ | |
| if (i !== 0 && lowercaseWords.has(lower)) { | |
| output.push(lower); | |
| continue; | |
| } | |
| /** | |
| * 3) Forced uppercase tokens (suffixes/credentials/roman numerals) | |
| * Examples: | |
| * - "jr" -> "JR" (or you can customize to "Jr" if preferred) | |
| * - "phd" -> "PHD" | |
| * | |
| * If you prefer "Jr" / "Sr" instead of all caps, see note below. | |
| */ | |
| if (forceUppercaseTokens.has(lower)) { | |
| output.push(lower.toUpperCase()); | |
| continue; | |
| } | |
| /** | |
| * 4) Default: capitalize this "word" while respecting apostrophes/hyphens | |
| */ | |
| output.push(capitalizeToken(raw)); | |
| } | |
| return output.join(" "); | |
| } | |
| /** | |
| * Cursor-safe update: | |
| * Writing to input.value can jump the caret to the end in many browsers. | |
| * This function attempts to preserve caret position for typical edits. | |
| * | |
| * Notes: | |
| * - If your users paste large strings or edit mid-text heavily, this still behaves well, | |
| * but no caret logic is perfect across every edge case. | |
| */ | |
| function setValuePreserveCaret($el, newValue) { | |
| const el = $el.get(0); | |
| if (!el) { | |
| $el.val(newValue); | |
| return; | |
| } | |
| const start = el.selectionStart; | |
| const end = el.selectionEnd; | |
| const oldValue = $el.val(); | |
| // Update only if there is an actual change | |
| if (oldValue === newValue) return; | |
| $el.val(newValue); | |
| // Best-effort caret preservation: keep the same selection range if possible | |
| try { | |
| const delta = newValue.length - oldValue.length; | |
| const newStart = Math.max(0, (start || 0) + delta); | |
| const newEnd = Math.max(0, (end || 0) + delta); | |
| el.setSelectionRange(newStart, newEnd); | |
| } catch (e) { | |
| // Some input types may not support selection ranges; ignore safely. | |
| } | |
| } | |
| /** | |
| * Event binding: | |
| * Using "input" ensures we catch: | |
| * - typing | |
| * - paste | |
| * - drag/drop | |
| * - mobile autofill adjustments | |
| */ | |
| $("#field_xxxxxx").on("input", function () { | |
| const $this = $(this); | |
| const original = $this.val(); | |
| const formatted = smartNameCase(original); | |
| setValuePreserveCaret($this, formatted); | |
| }); | |
| /** | |
| * Optional: if you only want to format on blur (when leaving the field), | |
| * comment out the "input" handler above and use this instead: | |
| * | |
| * $("#field_xxxxxx").on("blur", function () { | |
| * $(this).val(smartNameCase($(this).val())); | |
| * }); | |
| */ | |
| /** | |
| * Customization notes: | |
| * | |
| * 1) If you prefer "Jr" and "Sr" (instead of "JR"/"SR"): | |
| * - remove "jr" and "sr" from forceUppercaseTokens | |
| * - add a small special-case block: | |
| * if (lower === "jr") output.push("Jr"); | |
| * if (lower === "sr") output.push("Sr"); | |
| * | |
| * 2) Add more particles: | |
| * - Add to lowercaseWords (single words or two-word phrases) | |
| * Example: "st." is tricky; some names want "St. John" with "St." capitalized. | |
| * If you want to handle punctuation-based tokens, we can add a rule. | |
| * | |
| * 3) If you want to preserve ALL-CAPS input segments: | |
| * - We can detect tokens like "NASA" and leave them as-is when length > 1. | |
| */ | |
| }); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment