1// Shared validation utilities and common validators 2jQuery(function ($) { 3 // Common validators 4 validate.validators.mbMaxLength = function (value, options) { 5 if (!value) return; 6 7 // count mb_strwidth equivalent 8 const width = value.split("").reduce((sum, char) => { 9 return sum + (char.match(/[ -~]/) ? 1 : 2); // ASCII = 1, others = 2 10 }, 0); 11 12 if (width > options.max) { 13 return options.message || `${options.max / 2}æå以å ã§ãå ¥åãã ããã`; 14 } 15 }; 16 17 validate.validators.combinedMaxLength = function ( 18 value, 19 options, 20 key, 21 attributes, 22 ) { 23 const fields = options.fields || []; 24 let combined = ""; 25 26 fields.forEach((field) => { 27 const name = field.name; 28 const mode = field.mode || "value"; 29 30 if (mode === "value") { 31 combined += String(attributes[name] || ""); 32 return; 33 } 34 35 const el = document.querySelector(`[name="${name}"]`); 36 if (!el) return; 37 38 if (mode === "text" && el.tagName === "SELECT") { 39 if (el.selectedIndex < 0) return; 40 const opt = el.options[el.selectedIndex]; 41 42 if (!opt) return; 43 if (opt.value === '') return; 44 45 combined += opt.text || ''; 46 } 47 }); 48 49 return validate.validators.mbMaxLength(combined, options, key); 50 }; 51 52 validate.validators.noHalfKana = function (value) { 53 if (!value) return; 54 55 if (/[ï½§-ï¾ï¾ï¾]/u.test(value)) { 56 return "^åè§ã«ã¿ã«ãã¯å ¥åã§ãã¾ããã"; 57 } 58 }; 59 60 validate.validators.postcode = function (value) { 61 if (!value) return; 62 63 if (!/^([0-9]{3})(-?)([0-9]{4})$/u.test(value)) { 64 return "^éµä¾¿çªå·ã¯åè§æ°å7æ¡ã§å ¥åãã¦ãã ããã"; 65 } 66 }; 67 68 validate.validators.noEmoji = function (value) { 69 if (!value) return; 70 71 const emojiRegex = 72 /([\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F700}-\u{1F77F}\u{1F780}-\u{1F7FF}\u{1F800}-\u{1F8FF}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{2300}-\u{23FF}\u{2B50}\u{231A}\u{1F004}-\u{1F0CF}\u{2B06}\u{2194}\u{2195}\u{25AA}
72\u{25FE}\u{00AE}\u{00A9}\u{3030}])/u; 73 74 if (emojiRegex.test(value)) { 75 return "^çµµæåãæ©ç¨®ä¾åæåã¯å ¥åã§ãã¾ããã"; 76 } 77 }; 78 79 validate.validators.noMachineDependentCharacters = function (value) { 80 if (!value) return; 81 82 const machineDependentCharacters = 83 "â â¡â¢â£â¤â¥â¦â§â¨â©âªâ«â¬ââ®â¯â°â±â²â³â ⠡⠢⠣⠤⠥⠦⠧⠨⠩ããã¢ããã§ãã¶ãããã¦ã£ã«ãã»ããããããã¡ã»ããâãâ¡ã¤ã¥ã¦ã§ã¨ã±ã²ã¹ã¾ã½ã¼ã»ã¿"; 84 85 const machineDependentRegex = new RegExp( 86 `[${machineDependentCharacters.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}]`, 87 "u", 88 ); 89 90 if (machineDependentRegex.test(value)) { 91 return "^çµµæåãæ©ç¨®ä¾åæåã¯å ¥åã§ãã¾ããã"; 92 } 93 }; 94 95 function sanitizeJPPhone(value) { 96 // fullwidth â halfwidth number 97 value = value.replace(/[ï¼-ï¼]/g, (s) => 98 String.fromCharCode(s.charCodeAt(0) - 0xfee0), 99 ); 100 101 // remove non-digit 102 value = value.replace(/\D/g, ""); 103 104 // 810/81 â 0 105 if (value.startsWith("810")) value = "0" + value.slice(3); 106 else if (value.startsWith("81")) value = "0" + value.slice(2); 107 108 return value; 109 } 110 111 validate.validators.jpPhone = function (value) { 112 if (!value) return; 113 114 const unallowCharacters = /[^\s#0-9ï¼-ï¼_\-\+\/().]/; 115 if (unallowCharacters.test(value)) { 116 return "^é»è©±çªå·ã¯10æ¡ã¾ãã¯11æ¡ã®æ°åã§å ¥åãã¦ãã ããã"; 117 } 118 119 const v = sanitizeJPPhone(value); 120 if (!/^\d{10,11}$/.test(v)) { 121 return "^é»è©±çªå·ã¯10æ¡ã¾ãã¯11æ¡ã®æ°åã§å ¥åãã¦ãã ããã"; 122 } 123 }; 124 125 function wpIsEmail(email) { 126 if (!email) return false; 127 128 // Minimum length 129 if (email.length < 6) return false; 130 131 // Must contain '@' not at first position 132 const atPos = email.indexOf("@"); 133 if (atPos < 1) return false; 134 135 const local = email.slice(0, atPos); 136 const domain = email.slice(atPos + 1); 137 138 // LOCAL PART 139 if (!/^[a-zA-Z0-9!#$%&'*+\/=?^_`{|}~.\-]+$/.test(local)) { 140 return false; 141 } 142 143 // DOMAIN PART 144 if (/\.{2,}/.test(domain)) { 145 return false; 146 } 147 148 if (domain.trim(" \t\n\r\0\x0B.") !== domain) { 149 return false; 150 } 151 152 const subs = domain.split("."); 153 154 if (subs.length < 2) { 155 return false; 156 } 157 158 for (const sub of subs) { 159 if (sub.trim(" \t\n\r\0\x0B-") !== sub) { 160 return false; 161 } 162 163 if (!/^[a-z0-9-]+$/i.test(sub)) { 164 return false; 165 } 166 } 167 168 return true; 169 } 170 171 validate.validators.isEmail = function (value, options, key, attributes) { 172 if (!value) return; 173 if (!wpIsEmail(value)) { 174 return options.message || "ã¡ã¼ã«ã¢ãã¬ã¹ãæ£ããå ¥åãã¦ãã ããï¼ä¾ï¼[email protected]ï¼ã"; 175 } 176 }; 177 178 // Common validation helper functions 179 function addCommonConstraints(constraints, ignoreFields = []) { 180 Object.keys(constraints).forEach((key) => { 181 // Skip ignoreFields 182 if (ignoreFields.includes(key)) { 183 return; 184 } 185 186 const field = constraints[key]; 187 const rest = { ...field }; // clone old field 188 189 delete rest.noHalfKana; 190 delete rest.noEmoji; 191 delete rest.noMachineDependentCharacters; 192 193 constraints[key] = { 194 noHalfKana: true, 195 noEmoji: true, 196 noMachineDependentCharacters: true, 197 ...rest, 198 }; 199 }); 200 201 return constraints; 202 } 203 204 function addPresenceConstraints( 205 constraints, 206 fieldLabels = {}, 207 containerSelector = ".form-row", 208 ) { 209 document.querySelectorAll("label .required").forEach((abbr) => { 210 const label = abbr.closest("label"); 211 if (!label) return; 212 213 const input = label 214 .closest(containerSelector) 215 .querySelector("input, select, textarea"); 216 if (!input) return; 217 218 const name = input.getAttribute("name"); 219 if (!name) return; 220 221 // Only add constraint for EXIST fields in constraints 222 if (!Object.prototype.hasOwnProperty.call(constraints, name)) { 223 return; 224 } 225 226 const old = constraints[name] || {}; 227 let validateMsg = `^${fieldLabels[name] || name}ãå ¥åãã¦ãã ããã`; 228 // Customize the required validation message for the çªå° (address_1) fields. 229 if (["billing_address_1", "shipping_address_1"].includes(name)) { 230 validateMsg = `^${fieldLabels[name] || name}ãå ¥åãã¦ãã ãããçªå°ã®ãªãå°åºã«ãä½ã¾ãã®å ´åã¯ãçªå°ãªãã¨è¡¨è¨ããé¡ããããã¾ãã` 231 } 232 constraints[name] = { 233 presence: { 234 allowEmpty: false, 235 message: validateMsg, 236 }, 237 ...old, 238 }; 239 });
240 241 return constraints; 242 } 243 244 function getFormValues( 245 formSelector = "form.woocommerce-checkout, form.edit-account", 246 ) { 247 const form = document.querySelector(formSelector); 248 const values = {}; 249 250 if (form) { 251 new FormData(form).forEach((value, key) => { 252 values[key] = value.trim(); 253 }); 254 } 255 256 return values; 257 } 258 259 function showError( 260 fieldName, 261 messagesWithRules, 262 formContext = "", 263 containerSelector = ".form-row" 264 ) { 265 clearError(fieldName, formContext, containerSelector); 266 267 let ele = formContext 268 ? $(`${formContext} [name='${fieldName}']`) 269 : $(`form [name='${fieldName}']`); 270 const container = ele.closest(containerSelector); 271 272 if (container.length) { 273 container 274 .addClass("woocommerce-invalid") 275 .removeClass("woocommerce-validated"); 276 } 277 ele.addClass("shu-invalid").removeClass("woocommerce-validated"); 278 279 // Find existing error list or create new one 280 let errorList = container.find(`.field-error-list[data-field='${fieldName}']`); 281 282 if (errorList.length === 0) { 283 // Create new error list if doesn't exist 284 target = ele; 285 const showAfter = container.find(".show-before-message"); 286 if (showAfter.length) { 287 target = showAfter; 288 } 289 290 errorList = $(`<ul class='field-error-list' data-field='${fieldName}'></ul>`); 291 target.after(errorList); 292 } 293 294 // Add all messages with their rule types 295 const newMessages = messagesWithRules.map(item => { 296 return `<li data-id='${fieldName}' data-rule='${item.ruleType}'>${item.message}</li>`; 297 }).join(""); 298 299 errorList.append(newMessages); 300 } 301 302 function clearError( 303 fieldName, 304 formContext = "", 305 containerSelector = ".form-row" 306 ) { 307 let ele = formContext 308 ? $(`${formContext} [name='${fieldName}']`) 309 : $(`form [name='${fieldName}']`); 310 const container = ele.closest(containerSelector); 311 312 if (container.length) { 313 container 314 .removeClass("woocommerce-invalid") 315 .addClass("woocommerce-validated"); 316 } 317 ele.removeClass("shu-invalid").removeClass("woocommerce-validated"); 318 319 // Clear errors for this specific field 320 container.find(".field-error-list").remove(); 321 } 322 323 function clearRuleErrors(fieldName, ruleType, formContext, containerSelector) { 324 let ele = formContext 325 ? $(`${formContext} [name='${fieldName}']`) 326 : $(`form [name='${fieldName}']`); 327 const container = ele.closest(containerSelector); 328 329 if (container.length) { 330 // Clear specific rule errors - match exact rule ID or rule type prefix 331 container.find(`.field-error-list[data-field='${fieldName}'] li[data-rule="${ruleType}"]`).remove(); 332 333 // If no more errors, remove the error list 334 const remainingErrors = container.find(`.field-error-list[data-field='${fieldName}'] li`); 335 if (remainingErrors.length === 0) { 336 container.find(`.field-error-list[data-field='${fieldName}']`).remove(); 337 container.removeClass("woocommerce-invalid").addClass("woocommerce-validated"); 338 ele.removeClass("shu-invalid").addClass("woocommerce-validated"); 339 } 340 } 341 } 342 343 function clearValidateStatus( 344 fieldName, 345 formContext = "", 346 containerSelector = ".form-row", 347 ) { 348 let ele = formContext 349 ? $(`${formContext} [name='${fieldName}']`) 350 : $(`form [name='${fieldName}']`); 351 const container = ele.closest(containerSelector); 352 353 if (container.length) { 354 container 355 .removeClass("woocommerce-invalid") 356 .removeClass("woocommerce-validated"); 357 } 358 ele.removeClass("shu-invalid").removeClass("woocommerce-validated"); 359 ele.parent().find(".field-error-list").remove(); 360 } 361 362 function scrollToField(el) { 363 const adminBar = document.getElementById("wpadminbar"); 364 const adminHeight = adminBar ? adminBar.offsetHeight : 0; 365 366 const header = document.querySelector("header nav.navbar"); 367 const headerHeight = header ? header.offsetHeight : 0; 368 369 if (form) { 370 new FormData(form).forEach((value, key) => { 371 values[key] = value.trim(); 372 }); 373 } 374 375 return values; 376 } 377 378 function scrollToField(el) { 379 const adminBar = document.getElementById("wpadminbar"); 380 const adminHeight = adminBar ? adminBar.offsetHeight : 0; 381 382 const header = document.querySelector("header nav.navbar"); 383 const headerHeight = header ? header.offsetHeight : 0; 384 385 const offset = el.offset().top - adminHeight - headerHeight - 10; 386 387 $("html, body").animate({ scrollTop: offset }, 300); 388 } 389 390 function validateForm( 391 constraints, 392 formContext = "", 393 containerSelector = ".form-row", 394 ) { 395 // clear all error
396 Object.keys(constraints).forEach((fieldName) => { 397 clearError(fieldName, formContext, containerSelector); 398 }); 399 400 let isValid = true; 401 const values = getFormValues(formContext || "form"); 402 const errors = validate(values, constraints, {format: "detailed"}) || {}; 403 404 if (errors.length > 0) { 405 isValid = false; 406 407 // Group errors by field 408 const errorsByField = {}; 409 errors.forEach(error => { 410 if (!errorsByField[error.attribute]) { 411 errorsByField[error.attribute] = []; 412 } 413 errorsByField[error.attribute].push(error); 414 }); 415 416 Object.entries(errorsByField).forEach(([fieldName, fieldErrors]) => { 417 // Group all messages with their rule types 418 const messagesWithRules = fieldErrors.map(error => ({ 419 message: error.error, 420 ruleType: error.validator || 'general' 421 })).filter(item => item.message); 422 423 // Show all messages for this field at once 424 showError(fieldName, messagesWithRules, formContext, containerSelector); 425 }); 426 427 // focus + scroll to first error field 428 const firstError = errors[0]; 429 const selector = formContext 430 ? `${formContext} [name='${firstError.attribute}']` 431 : `form [name='${firstError.attribute}']`; 432 const el = $(selector); 433 if (el.length) { 434 el.focus(); 435 if (el.closest(containerSelector)) { 436 scrollToField(el.closest(containerSelector)); 437 } 438 } 439 } 440 441 return isValid; 442 } 443 444 function validateField( 445 constraints, 446 fieldName, 447 formContext = "", 448 containerSelector = ".form-row", 449 ) { 450 const values = getFormValues(formContext || "form"); 451 const errors = validate(values, constraints, {format: "detailed"}) || {}; 452 453 // Get all errors for this field 454 const fieldErrors = Array.isArray(errors) ? errors.filter(error => error.attribute === fieldName) : []; 455 456 if (fieldErrors.length > 0) { 457 // Group all messages with their rule types 458 const messagesWithRules = fieldErrors.map(error => ({ 459 message: error.error, 460 ruleType: error.validator || 'general' 461 })).filter(item => item.message); 462 463 // Show all messages at once with their rule types 464 showError(fieldName, messagesWithRules, formContext, containerSelector); 465 } else { 466 clearError(fieldName, formContext, containerSelector); 467 // Clear combined errors for all fields in the same combined group 468 clearCombinedErrors(constraints, fieldName, errors, formContext, containerSelector); 469 } 470 } 471 472 function clearCombinedErrors(constraints, fieldName, errors, formContext, containerSelector) { 473 const fieldConstraint = constraints[fieldName]; 474 if (!fieldConstraint || !fieldConstraint.combinedMaxLength || !fieldConstraint.combinedMaxLength.fields) { 475 return; 476 } 477 478 const combinedFields = fieldConstraint.combinedMaxLength.fields; 479 combinedFields.forEach(field => { 480 const combinedFieldName = typeof field === 'object' ? field.name : field; 481 482 // Check if this field has combined errors in the detailed errors array 483 const hasCombinedError = Array.isArray(errors) ? 484 errors.some(error => error.attribute === combinedFieldName && error.validator === 'combinedMaxLength') : false; 485 486 if (!hasCombinedError) { 487 // Clear combined errors for this field 488 clearRuleErrors(combinedFieldName, 'combinedMaxLength', formContext, containerSelector); 489 } 490 }); 491 } 492 493 // Export for use in other modules 494 window.ValidationUtils = { 495 addCommonConstraints, 496 addPresenceConstraints, 497 getFormValues, 498 showError, 499 clearError, 500 clearRuleErrors, 501 clearValidateStatus, 502 scrollToField, 503 validateForm, 504 validateField, 505 }; 506});
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.