Skip to content

Instantly share code, notes, and snippets.

@vfontjr
Created January 25, 2026 21:41
Show Gist options
  • Select an option

  • Save vfontjr/095de80f21922b7a3570ef41a765e313 to your computer and use it in GitHub Desktop.

Select an option

Save vfontjr/095de80f21922b7a3570ef41a765e313 to your computer and use it in GitHub Desktop.
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