PageSourceSearch

https://morvarid.pub/assets/frappe/js/portal_field_validation.js?v=20260715b

js morvarid.pub collected 2026-10-02 14:13:40 UTC 8,180 bytes, 304 lines download raw bytes

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.