PageSourceSearch

https://www.celes-perfume.com/wp-content/themes/shop-isle-pro/cust…/validation-utils.js?1791066305&ver=1.0.0

js celes-perfume.com collected 2026-10-03 22:26:54 UTC 15,861 bytes, 506 lines download raw bytes

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(/[0-9]/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-90-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.