1// Portal / web shop: normalization + optional client hints (English only). 2// Authoritative validation for cart address fields runs in Python (portal_field_validation.py). 3(function () { 4 var IR_MOBILE_RE = /^09\d{9}$/; 5 var IR_POSTAL_LEN = 10; 6 var EMAIL_LOOSE_RE = 7 /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i; 8 9 var ARABIC_MARKS_RE = /[\u0640\u064B-\u065F\u0670]/g; 10 11 function normalizePersianArabicLetters(str) { 12 var s = str == null ? "" : String(str).trim(); 13 if (!s) { 14 return ""; 15 } 16 try { 17 s = s.normalize("NFKC"); 18 } catch (e) { 19 /* ignore */ 20 } 21 s = s 22 .replace(/\u064A/g, "\u06CC") 23 .replace(/\u0649/g, "\u06CC") 24 .replace(/\u0643/g, "\u06A9") 25 .replace(/\u0629/g, "\u0647") 26 .replace(/\u0623/g, "\u0627") 27 .replace(/\u0625/g, "\u0627") 28 .replace(/\u0671/g, "\u0627") 29 .replace(/\u0624/g, "\u0648") 30 .replace(/\u0626/g, "\u06CC") 31 .replace(/\u06D2/g, "\u06CC") 32 .replace(/\u06C0/g, "\u0647"); 33 s = s.replace(ARABIC_MARKS_RE, ""); 34 return s; 35 } 36 37 function isNdChar(ch) { 38 try { 39 return /\p{Nd}/u.test(ch); 40 } catch (e) { 41 var code = ch.charCodeAt(0); 42 return ( 43 (code >= 48 && code <= 57) || 44 (code >= 0x06f0 && code <= 0x06f9) || 45 (code >= 0x0660 && code <= 0x0669) 46 ); 47 } 48 } 49 50 function isLetterChar(ch) { 51 try { 52 return /\p{L}/u.test(ch); 53 } catch (e) { 54 return /[A-Za-z\u0600-\u06FF]/.test(ch); 55 } 56 } 57 58 function normalizeToAsciiDigits(str) { 59 var s = str == null ? "" : String(str); 60 var out = ""; 61 for (var i = 0; i < s.length; i++) { 62 var ch = s[i]; 63 var code = ch.charCodeAt(0); 64 if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r" || ch === "\u200c") { 65 continue; 66 } 67 if (code >= 48 && code <= 57) { 68 out += ch; 69 } else if (code >= 0x0660 && code <= 0x0669) { 70 out += String.fromCharCode(code - 0x0660 + 48); 71 } else if (code >= 0x06f0 && code <= 0x06f9) { 72 out += String.fromCharCode(code - 0x06f0 + 48); 73 } else if (isNdChar(ch)) { 74 var d = parseInt(ch, 10); 75 if (!isNaN(d) && d >= 0 && d <= 9) { 76 out += String(d); 77 } 78 } 79 } 80 return out; 81 } 82 83 function validateIrMobile(raw) { 84 var n = normalizeToAsciiDigits(raw); 85 if (!IR_MOBILE_RE.test(n)) { 86 return { 87 ok: false, 88 message: 89 "Mobile number must be 11 digits starting with 09 (example: 09123456789).", 90 }; 91 } 92 return { ok: true, normalized: n }; 93 } 94 95 function validateIrPostal(raw) { 96 var n = normalizeToAsciiDigits(raw); 97 if (n.length !== IR_POSTAL_LEN) { 98 return { 99 ok: false, 100 message: "کد Ù¾Ø³ØªÛ Ø¨Ø§ÛØ¯ دÙÛÙØ§ " + String(IR_POSTAL_LEN) + " رÙ٠باشد", 101 title: "کد Ù¾Ø³ØªÛ ÙØ§Ù عتبر", 102 }; 103 } 104 return { ok: true, normalized: n }; 105 } 106 107 /** Show a client validation error with title + message. */ 108 function showPortalValidationError(result) { 109 if (!result || result.ok) { 110 return; 111 } 112 if (typeof frappe !== "undefined" && typeof frappe.msgprint === "function") { 113 frappe.msgprint({ 114 title: result.title || "Ù¾ÛØ§Ù ", 115 message: result.message || "", 116 indicator: "red", 117 }); 118 } 119 } 120 121 function validatePortalEmail(email, required) { 122 var s = email == null ? "" : String(email).trim(); 123 if (!s) { 124 if (required) { 125 return { 126 ok: false, 127 message: "Email address is required", 128 }; 129 } 130 return { ok: true }; 131 } 132 if (!EMAIL_LOOSE_RE.test(s)) { 133 return { 134 ok: false, 135 message: s + " is not a valid email address", 136 }; 137 } 138 return { ok: true }; 139 } 140 141 function isValidPersonNameComponent(value) { 142 var s = normalizePersianArabicLetters(value); 143 if (!s || s.indexOf("*") >= 0) { 144 return false; 145 } 146 var hasLetter = false; 147 for (var k = 0; k < s.length; k++) { 148 var ch = s[k]; 149 if (/\s/.test(ch)) { 150 continue; 151 } 152 if (isNdChar(ch)) { 153 return false; 154 } 155 if (isLetterChar(ch)) { 156 hasLetter = true; 157 } 158 } 159 return hasLetter; 160 } 161 162 function validateCompositePersonNamePortal(fullName) { 163 var t = normalizePersianArabicLetters(fullName); 164 if (!t) { 165 return { ok: false, message: "Please enter your full name." }; 166 } 167 var parts = t.split(/\s+/); 168 if (!isValidPersonNameComponent(parts[0])) { 169 return { 170 ok: false, 171 message: 172 "First name cannot contain numbers or asterisk (*), and must include at least one letter.", 173 }; 174 } 175 if (parts.length >= 2) { 176 var lastPart = parts.slice(1).join(" "); 177 if (!isValidPersonNameComponent(lastPart)) { 178 return { 179 ok: false, 180 message: 181 "Last name cannot contain numbers or asterisk (*), and must include at least one letter.", 182 }; 183 } 184 } 185 return { ok: true, normalized: t }; 186 } 187 188 function validatePortalAddressTitle(value) { 189 var t = normalizePersianArabicLetters(value); 190 if (!t) { 191 return { ok: false, message: "Address title is required." }; 192 } 193 if (!isValidPersonNameComponent(t)) { 194 return { 195 ok: false, 196 message: 197 "Address title cannot contain numbers or asterisk (*), and must include at least one letter.", 198 }; 199 } 200 return { ok: true, normalized: t }; 201 } 202 203 function stripDigitsFromText(str) { 204 var s = str == null ? "" : String(str); 205 var out = ""; 206 for (var i = 0; i < s.length; i++) { 207 if (!isNdChar(s[i])) { 208 out += s[i]; 209 } 210 } 211 return out; 212 } 213 214 /** Block typing / pasting digits (ASCII + Fa/Ar) into name/title inputs. */
215 function bindNoDigitNameInput(el) { 216 if (!el || el.getAttribute("data-no-digit-bound") === "1") { 217 return; 218 } 219 el.setAttribute("data-no-digit-bound", "1"); 220 el.setAttribute("inputmode", "text"); 221 function clean() { 222 var next = stripDigitsFromText(el.value); 223 if (next === el.value) { 224 return; 225 } 226 var start = el.selectionStart; 227 var end = el.selectionEnd; 228 var diff = el.value.length - next.length; 229 el.value = next; 230 if (typeof start === "number" && typeof el.setSelectionRange === "function") { 231 el.setSelectionRange(Math.max(0, start - diff), Math.max(0, end - diff)); 232 } 233 } 234 el.addEventListener("input", clean); 235 el.addEventListener("paste", function () { 236 setTimeout(clean, 0); 237 }); 238 } 239 240 function bindNoDigitNameInputs(root) { 241 var scope = root && root.querySelectorAll ? root : document; 242 var nodes = scope.querySelectorAll( 243 '#editTitle, input[id^="editTitle-"], input[data-no-digit-name="1"]' 244 ); 245 for (var i = 0; i < nodes.length; i++) { 246 bindNoDigitNameInput(nodes[i]); 247 } 248 } 249 250 /** 251 * Cart checkout: normalize digits / name script only. Do not block submit â 252 * Python (apply_validated_cart_address_fields) throws translated errors. 253 */ 254 function normalizeCartAddressValues(values) { 255 if (!values || typeof values !== "object") { 256 return; 257 } 258 if (values.username != null) { 259 values.username = normalizePersianArabicLetters( 260 stripDigitsFromText(values.username) 261 ); 262 } 263 if (values.address_title != null) { 264 values.address_title = normalizePersianArabicLetters( 265 stripDigitsFromText(values.address_title) 266 ); 267 } 268 if (values.phone != null) { 269 var pn = normalizeToAsciiDigits(values.phone); 270 if (pn) { 271 values.phone = pn; 272 } 273 } 274 if (values.pincode != null) { 275 var pc = normalizeToAsciiDigits(values.pincode); 276 if (pc) { 277 values.pincode = pc; 278 } 279 } 280 } 281 282 /** @deprecated Use normalizeCartAddressValues; kept for older templates. */ 283 function validateCartAddressValues(values) { 284 normalizeCartAddressValues(values); 285 return { ok: true }; 286 } 287 288 window.frappe_validate_portal_fields = { 289 normalizePersianArabicLetters: normalizePersianArabicLetters, 290 normalizeToAsciiDigits: normalizeToAsciiDigits, 291 stripDigitsFromText: stripDigitsFromText, 292 bindNoDigitNameInput: bindNoDigitNameInput, 293 bindNoDigitNameInputs: bindNoDigitNameInputs, 294 validateIrMobile: validateIrMobile, 295 validatePortalEmail: validatePortalEmail, 296 validateIrPostal: validateIrPostal, 297 showPortalValidationError: showPortalValidationError, 298 isValidPersonNameComponent: isValidPersonNameComponent, 299 validateCompositePersonName: validateCompositePersonNamePortal, 300 validatePortalAddressTitle: validatePortalAddressTitle, 301 normalizeCartAddressValues: normalizeCartAddressValues, 302 validateCartAddressValues: validateCartAddressValues, 303 }; 304})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.