diff --git a/themes/develop/package-lock.json b/themes/develop/package-lock.json index 0276cc5..63781fc 100644 --- a/themes/develop/package-lock.json +++ b/themes/develop/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "@ablogcms/pdf": "^3.2.29", + "@ablogcms/validator": "^3.2.29", "a-dispatcher": "^1.0.1", "alpinejs": "^3.16.3", "document-outliner": "^1.1.3", @@ -59,6 +60,12 @@ "pdfjs-dist": "^6.2.108" } }, + "node_modules/@ablogcms/validator": { + "version": "3.2.32", + "resolved": "https://registry.npmjs.org/@ablogcms/validator/-/validator-3.2.32.tgz", + "integrity": "sha512-vqozZrIwJbT2+VtSs6C4iCKTMZ/ZHA2qCCIvQ+015hnGHNsLN/keEyWVTMoMygY0g+mn4qDLhQVON0QK5TPUtA==", + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", diff --git a/themes/develop/package.json b/themes/develop/package.json index 7e98a40..38b5ecb 100644 --- a/themes/develop/package.json +++ b/themes/develop/package.json @@ -42,6 +42,7 @@ }, "dependencies": { "@ablogcms/pdf": "^3.2.29", + "@ablogcms/validator": "^3.2.29", "a-dispatcher": "^1.0.1", "alpinejs": "^3.16.3", "document-outliner": "^1.1.3", diff --git a/themes/develop/src/js/lib/build-in.js b/themes/develop/src/js/lib/build-in.js index 6872248..35f767b 100644 --- a/themes/develop/src/js/lib/build-in.js +++ b/themes/develop/src/js/lib/build-in.js @@ -8,7 +8,7 @@ import { linkMatch, linkMatchFull, linkMatchContain } from './buildIn/link-match * Validator * @param {Document | Element} context * @param {string} selector - * @param {import("./buildIn/validator/types").ValidatorOptions} options + * @param {import("@ablogcms/validator/types").ValidatorOptions} options */ const validator = (context, selector, options = {}) => { domContentLoaded(async () => { diff --git a/themes/develop/src/js/lib/buildIn/validator-fn.js b/themes/develop/src/js/lib/buildIn/validator-fn.js index 9934685..2b69c47 100644 --- a/themes/develop/src/js/lib/buildIn/validator-fn.js +++ b/themes/develop/src/js/lib/buildIn/validator-fn.js @@ -1,4 +1,4 @@ -import Validator from './validator'; +import Validator from '@ablogcms/validator'; export default (elm, options = {}) => { new Validator(elm, options); diff --git a/themes/develop/src/js/lib/buildIn/validator/index.js b/themes/develop/src/js/lib/buildIn/validator/index.js deleted file mode 100644 index ff00bb8..0000000 --- a/themes/develop/src/js/lib/buildIn/validator/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import Validator from './validator'; - -export default Validator; diff --git a/themes/develop/src/js/lib/buildIn/validator/rules/allJustChecked.js b/themes/develop/src/js/lib/buildIn/validator/rules/allJustChecked.js deleted file mode 100644 index 379c221..0000000 --- a/themes/develop/src/js/lib/buildIn/validator/rules/allJustChecked.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Validates if exactly the specified number of checkboxes are checked - * @type {import('../types').ValidationRule} - */ -export const allJustChecked = (_, num, checkboxes) => { - if (!Array.isArray(checkboxes)) { - return true; - } - if (!checkboxes.every((checkbox) => checkbox instanceof HTMLInputElement)) { - return true; - } - return parseInt(num, 10) === checkboxes.filter(({ checked }) => checked).length; -}; diff --git a/themes/develop/src/js/lib/buildIn/validator/rules/allMaxChecked.js b/themes/develop/src/js/lib/buildIn/validator/rules/allMaxChecked.js deleted file mode 100644 index 3d74a70..0000000 --- a/themes/develop/src/js/lib/buildIn/validator/rules/allMaxChecked.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Validates if the number of checked checkboxes is less than or equal to the specified maximum - * @type {import('../types').ValidationRule} - */ -export const allMaxChecked = (_, num, checkboxes) => { - if (!Array.isArray(checkboxes)) { - return true; - } - if (!checkboxes.every((checkbox) => checkbox instanceof HTMLInputElement)) { - return true; - } - return parseInt(num, 10) >= checkboxes.filter(({ checked }) => checked).length; -}; diff --git a/themes/develop/src/js/lib/buildIn/validator/rules/allMinChecked.js b/themes/develop/src/js/lib/buildIn/validator/rules/allMinChecked.js deleted file mode 100644 index 1a66b26..0000000 --- a/themes/develop/src/js/lib/buildIn/validator/rules/allMinChecked.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Validates if the number of checked checkboxes is greater than or equal to the specified minimum - * @type {import('../types').ValidationRule} - */ -export const allMinChecked = (_, num, checkboxes) => { - if (!Array.isArray(checkboxes)) { - return true; - } - if (!checkboxes.every((checkbox) => checkbox instanceof HTMLInputElement)) { - return true; - } - return parseInt(num, 10) <= checkboxes.filter(({ checked }) => checked).length; -}; diff --git a/themes/develop/src/js/lib/buildIn/validator/rules/dates.js b/themes/develop/src/js/lib/buildIn/validator/rules/dates.js deleted file mode 100644 index f5db718..0000000 --- a/themes/develop/src/js/lib/buildIn/validator/rules/dates.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Validates if the value matches various date formats - * @type {import('../types').ValidationRule} - */ -export const dates = (val) => { - if (!val) { - return true; - } - return /^[sS]{1,2}(\d{2})\W{1}\d{1,2}\W{1}\d{0,2}$|^[hH]{1}(\d{1,2})\W{1}\d{1,2}\W{1}\d{0,2}$|^\d{1,2}$|^\d{1,2}\W{1}\d{1,2}$|^\d{2,4}\W{1}\d{1,2}\W{1}\d{0,2}$|^\d{4}\d{2}\d{2}/.test( - val - ); -}; diff --git a/themes/develop/src/js/lib/buildIn/validator/rules/digits.js b/themes/develop/src/js/lib/buildIn/validator/rules/digits.js deleted file mode 100644 index da9fb36..0000000 --- a/themes/develop/src/js/lib/buildIn/validator/rules/digits.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Validates if the value contains only digits - * @type {import('../types').ValidationRule} - */ -export const digits = (val) => { - if (!val) { - return true; - } - return val === String(parseInt(val, 10)); -}; diff --git a/themes/develop/src/js/lib/buildIn/validator/rules/email.js b/themes/develop/src/js/lib/buildIn/validator/rules/email.js deleted file mode 100644 index 95b107a..0000000 --- a/themes/develop/src/js/lib/buildIn/validator/rules/email.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Email validation rule - * @type {import('../types').ValidationRule} - */ -export const email = (val) => { - if (!val) { - return true; - } - // eslint-disable-next-line no-useless-escape - return /^(?:(?:(?:(?:[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+)(?:\.(?:[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+))*)|(?:"(?:\\[^\r\n]|[^\\"])*")))\@(?:(?:(?:(?:[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+)(?:\.(?:[a-zA-Z0-9_!#\$\%&'*+/=?\^`{}~|\-]+))*)|(?:\[(?:\\\S|[\x21-\x5a\x5e-\x7e])*\])))$/.test( - val - ); -}; diff --git a/themes/develop/src/js/lib/buildIn/validator/rules/equalTo.js b/themes/develop/src/js/lib/buildIn/validator/rules/equalTo.js deleted file mode 100644 index 189c9d9..0000000 --- a/themes/develop/src/js/lib/buildIn/validator/rules/equalTo.js +++ /dev/null @@ -1,35 +0,0 @@ -import { getFormElements } from '../utils'; - -/** - * Validates if the value equals to another input's value - * @type {import('../types').ValidationRule} - */ -export const equalTo = (val, name, input) => { - if (Array.isArray(input)) { - return true; - } - - if (input.form === null) { - return true; - } - - const input2 = getFormElements(input.form).find((element) => element.name === name); - - if (input2 === null) { - // バリデーション対象の要素がフォームの要素ではない場合は無効なルールとしてtrueを返す - return true; - } - - if ( - !(input2 instanceof HTMLInputElement) && - !(input2 instanceof HTMLSelectElement) && - !(input2 instanceof HTMLTextAreaElement) && - !(input2 instanceof HTMLButtonElement) && - !(input2 instanceof RadioNodeList) - ) { - // バリデーション対象の要素がフォームの要素ではない場合は無効なルールとしてtrueを返す - return true; - } - - return val === input2.value; -}; diff --git a/themes/develop/src/js/lib/buildIn/validator/rules/filesize.js b/themes/develop/src/js/lib/buildIn/validator/rules/filesize.js deleted file mode 100644 index f26b1ed..0000000 --- a/themes/develop/src/js/lib/buildIn/validator/rules/filesize.js +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Validates if the file size is within the specified limit - * @type {import('../types').ValidationRule} - */ -export const filesize = (_, max, input) => { - if (!(input instanceof HTMLInputElement)) { - return true; - } - if (!input.files) { - return true; - } - if (input.files.length < 1) { - return true; - } - if (input.files[0].size > parseInt(max, 10) * 1024) { - return false; - } - return true; -}; diff --git a/themes/develop/src/js/lib/buildIn/validator/rules/hiragana.js b/themes/develop/src/js/lib/buildIn/validator/rules/hiragana.js deleted file mode 100644 index a10c651..0000000 --- a/themes/develop/src/js/lib/buildIn/validator/rules/hiragana.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Validates if the value contains only hiragana characters - * @type {import('../types').ValidationRule} - */ -export const hiragana = (val) => { - if (!val) { - return true; - } - if (val.match(/^[ぁ-ゞー]+$/)) { - return true; - } - return false; -}; diff --git a/themes/develop/src/js/lib/buildIn/validator/rules/index.js b/themes/develop/src/js/lib/buildIn/validator/rules/index.js deleted file mode 100644 index 1ca137b..0000000 --- a/themes/develop/src/js/lib/buildIn/validator/rules/index.js +++ /dev/null @@ -1,19 +0,0 @@ -export { required } from './required'; -export { minlength } from './minlength'; -export { maxlength } from './maxlength'; -export { min } from './min'; -export { max } from './max'; -export { regex } from './regex'; -export { regexp } from './regexp'; -export { digits } from './digits'; -export { equalTo } from './equalTo'; -export { katakana } from './katakana'; -export { hiragana } from './hiragana'; -export { allJustChecked as all_justChecked } from './allJustChecked'; -export { allMinChecked as all_minChecked } from './allMinChecked'; -export { allMaxChecked as all_maxChecked } from './allMaxChecked'; -export { dates } from './dates'; -export { times } from './times'; -export { url } from './url'; -export { email } from './email'; -export { filesize } from './filesize'; diff --git a/themes/develop/src/js/lib/buildIn/validator/rules/katakana.js b/themes/develop/src/js/lib/buildIn/validator/rules/katakana.js deleted file mode 100644 index c130244..0000000 --- a/themes/develop/src/js/lib/buildIn/validator/rules/katakana.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Validates if the value contains only katakana characters - * @type {import('../types').ValidationRule} - */ -export const katakana = (val) => { - if (!val) { - return true; - } - if (val.match(/^[ァ-ヾー]+$/)) { - return true; - } - return false; -}; diff --git a/themes/develop/src/js/lib/buildIn/validator/rules/max.js b/themes/develop/src/js/lib/buildIn/validator/rules/max.js deleted file mode 100644 index 1000f8b..0000000 --- a/themes/develop/src/js/lib/buildIn/validator/rules/max.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Validates if the value is less than or equal to the specified maximum - * @type {import('../types').ValidationRule} - */ -export const max = (val, num) => { - if (!val) { - return true; - } - return parseInt(num, 10) >= parseInt(val, 10); -}; diff --git a/themes/develop/src/js/lib/buildIn/validator/rules/maxlength.js b/themes/develop/src/js/lib/buildIn/validator/rules/maxlength.js deleted file mode 100644 index 7182764..0000000 --- a/themes/develop/src/js/lib/buildIn/validator/rules/maxlength.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Validates if the value's length is less than or equal to the specified maximum - * @type {import('../types').ValidationRule} - */ -export const maxlength = (val, len) => { - if (!val) { - return true; - } - return parseInt(len, 10) >= String(val).length; -}; diff --git a/themes/develop/src/js/lib/buildIn/validator/rules/min.js b/themes/develop/src/js/lib/buildIn/validator/rules/min.js deleted file mode 100644 index f772489..0000000 --- a/themes/develop/src/js/lib/buildIn/validator/rules/min.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Validates if the value is greater than or equal to the specified minimum - * @type {import('../types').ValidationRule} - */ -export const min = (val, num) => { - if (!val) { - return true; - } - return parseInt(num, 10) <= parseInt(val, 10); -}; diff --git a/themes/develop/src/js/lib/buildIn/validator/rules/minlength.js b/themes/develop/src/js/lib/buildIn/validator/rules/minlength.js deleted file mode 100644 index 8fb0c43..0000000 --- a/themes/develop/src/js/lib/buildIn/validator/rules/minlength.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Minimum length validation rule - * @type {import('../types').ValidationRule} - */ -export const minlength = (val, len) => { - if (!val) { - return true; - } - return parseInt(len, 10) <= String(val).length; -}; diff --git a/themes/develop/src/js/lib/buildIn/validator/rules/regex.js b/themes/develop/src/js/lib/buildIn/validator/rules/regex.js deleted file mode 100644 index 2f97128..0000000 --- a/themes/develop/src/js/lib/buildIn/validator/rules/regex.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Validates if the value matches the specified regular expression - * @type {import('../types').ValidationRule} - */ -export const regex = (val, regex) => { - if (!val) { - return true; - } - let flag = ''; - let re = regex; - if (regex.match(/^@(.*)@([igm]*)$/)) { - re = RegExp.$1; - flag = RegExp.$2; - } - return new RegExp(re, flag).test(val); -}; diff --git a/themes/develop/src/js/lib/buildIn/validator/rules/regexp.js b/themes/develop/src/js/lib/buildIn/validator/rules/regexp.js deleted file mode 100644 index 0034fff..0000000 --- a/themes/develop/src/js/lib/buildIn/validator/rules/regexp.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Alias for regex validation rule - * @type {import('../types').ValidationRule} - */ -import { regex } from './regex'; - -export const regexp = (val, pattern, input, v) => { - return regex(val, pattern, input, v); -}; diff --git a/themes/develop/src/js/lib/buildIn/validator/rules/required.js b/themes/develop/src/js/lib/buildIn/validator/rules/required.js deleted file mode 100644 index 9f97733..0000000 --- a/themes/develop/src/js/lib/buildIn/validator/rules/required.js +++ /dev/null @@ -1,22 +0,0 @@ -import { getFormElements } from '../utils'; - -/** - * Required validation rule - * @type {import('../types').ValidationRule} - */ -export const required = (val, _, input) => { - if (Array.isArray(input)) { - return true; - } - if (input.type === 'checkbox' || input.type === 'radio') { - if (input.form === null) { - return true; - } - const inputs = getFormElements(input.form) - .filter((element) => element.name === input.name) - .filter((element) => !element.disabled); - return inputs.some(({ checked }) => checked === true); - } - - return !!val; -}; diff --git a/themes/develop/src/js/lib/buildIn/validator/rules/times.js b/themes/develop/src/js/lib/buildIn/validator/rules/times.js deleted file mode 100644 index 7e40112..0000000 --- a/themes/develop/src/js/lib/buildIn/validator/rules/times.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Time format validation rule - * @type {import('../types').ValidationRule} - */ -export const times = (val) => { - if (!val) { - return true; - } - - // Check format using original regex pattern and add HH:MM:ss pattern - if ( - !/^\d{1,2}$|^\d{1,2}\W{1}\d{1,2}$|^\d{1,2}\W{1}\d{1,2}\W{1}\d{1,2}$|^\d{2}\d{2}\d{2}$|^\d{2}:\d{2}(:\d{2})?$/.test( - val - ) - ) { - return false; - } - - // Extract time components - let hours; - let minutes; - let seconds = 0; - - if (val.includes(':')) { - const parts = val.split(':'); - hours = Number(parts[0]); - minutes = Number(parts[1]); - if (parts.length === 3) { - seconds = Number(parts[2]); - } - } else if (val.length === 6) { - // Handle HHMMSS format - hours = Number(val.substring(0, 2)); - minutes = Number(val.substring(2, 4)); - seconds = Number(val.substring(4, 6)); - } else if (val.length === 4) { - // Handle HHMM format - hours = Number(val.substring(0, 2)); - minutes = Number(val.substring(2, 4)); - } else { - // Handle single or double digit format - hours = Number(val); - minutes = 0; - } - - // Check if hours are between 0-23, minutes are between 0-59, and seconds are between 0-59 - return hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59 && seconds >= 0 && seconds <= 59; -}; diff --git a/themes/develop/src/js/lib/buildIn/validator/rules/url.js b/themes/develop/src/js/lib/buildIn/validator/rules/url.js deleted file mode 100644 index 5a6576c..0000000 --- a/themes/develop/src/js/lib/buildIn/validator/rules/url.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Validates if the value is a valid URL - * @type {import('../types').ValidationRule} - */ -export const url = (val) => { - if (!val) { - return true; - } - // eslint-disable-next-line no-useless-escape - return /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test( - val - ); -}; diff --git a/themes/develop/src/js/lib/buildIn/validator/types.js b/themes/develop/src/js/lib/buildIn/validator/types.js deleted file mode 100644 index dfb53b1..0000000 --- a/themes/develop/src/js/lib/buildIn/validator/types.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * バリデーターの設定オプション - * @typedef {Object} ValidatorOptions - * @property {string} resultClassName - バリデーション結果のクラス名のプレフィックス - * @property {string} okClassName - バリデーション成功時に付与されるクラス名 - * @property {string} ngClassName - バリデーション失敗時に付与されるクラス名 - * @property {boolean} shouldScrollOnSubmitFailed - バリデーション失敗時にスクロールするかどうか - * @property {boolean} shouldFocusOnSubmitFailed - バリデーション失敗時に要素をフォーカスするかどうか - * @property {function(ValidationResult[], HTMLFormElement): void} onSubmitFailed - 送信時のバリデーション失敗時のコールバック関数 - * @property {function(ValidationResult[], HTMLElement): void} onValid - バリデーション成功時のコールバック関数 - * @property {function(ValidationResult[], HTMLElement): void} onInvalid - バリデーション失敗時のコールバック関数 - * @property {function(ValidationResult[], HTMLElement): void} onValidated - バリデーション後に実行されるコールバック関数 - * @property {'onBlur' | 'onChange' | false} shouldValidate - バリデーションのタイミング - * @property {'onBlur' | 'onChange' | false} shouldRevalidate - 再バリデーションのタイミング - * @property {boolean} shouldValidateOnSubmit - 送信時にバリデーションを実行するかどうか - * @property {Record} customRules - カスタムバリデーションルール - */ - -/** - * バリデーションルールの定義 - * @typedef {Object} Validation - * @property {string} field - バリデーション対象のフィールド名 - * @property {keyof typeof rules} rule - 適用するバリデーションルール - * @property {string} arg - バリデーションルールの引数 - * @property {string} id - バリデーション要素のid属性 - * @property {string} name - バリデーション要素のname属性 - */ - -/** - * バリデーション結果 - * @typedef {Object} ValidationResult - * @property {boolean} ok - バリデーションが成功したかどうか - * @property {HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLButtonElement | - * (HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLButtonElement)[]} element - バリデーション対象の要素 - * @property {Validation} validation - 適用されたバリデーションルール - */ - -/** - * バリデーションルールの型定義 - * @typedef {function(string, string, HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLButtonElement | - * (HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLButtonElement)[], Validation): boolean} ValidationRule - */ diff --git a/themes/develop/src/js/lib/buildIn/validator/utils.js b/themes/develop/src/js/lib/buildIn/validator/utils.js deleted file mode 100644 index 93e16ed..0000000 --- a/themes/develop/src/js/lib/buildIn/validator/utils.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * 与えられた値が関数であるかを判定する Type Guard - * @template T - * @param {T} value - 判定対象の値 - * @returns {value is Function} `true` の場合、`value` は `Function` 型 - */ -function isFunction(value) { - return typeof value === 'function'; -} - -/** - * フォームが紐付けられたフィールドを取得 - * @param {HTMLFormElement} form - フォーム - * @returns {(HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLButtonElement)[]} フィールドの配列 - */ -function getFormElements(form) { - // フォーム内のフィールドを取得 - const internalFields = Array.from(form.elements).filter((element) => !(element instanceof HTMLFieldSetElement)); - - // form属性で紐付けられた外部フィールドを取得 - const externalFields = Array.from(document.querySelectorAll(`[form="${form.id}"]`)).filter( - (element) => - element instanceof HTMLInputElement || - element instanceof HTMLSelectElement || - element instanceof HTMLTextAreaElement || - element instanceof HTMLButtonElement - ); - - return [...internalFields, ...externalFields]; -} - -export { isFunction, getFormElements }; diff --git a/themes/develop/src/js/lib/buildIn/validator/validator.js b/themes/develop/src/js/lib/buildIn/validator/validator.js deleted file mode 100644 index a91e339..0000000 --- a/themes/develop/src/js/lib/buildIn/validator/validator.js +++ /dev/null @@ -1,498 +0,0 @@ -import * as rules from './rules'; -import { getFormElements, isFunction } from './utils'; - -/** - * @type {import("./types").ValidatorOptions} - */ -const defaultOptions = { - resultClassName: 'validator-result-', - okClassName: 'valid', - ngClassName: 'invalid', - shouldScrollOnSubmitFailed: true, - shouldFocusOnSubmitFailed: true, - onInvalid: () => {}, - onValid: () => {}, - onValidated: () => {}, - onSubmitFailed: () => {}, - shouldValidate: 'onBlur', - shouldRevalidate: 'onChange', - shouldValidateOnSubmit: true, - customRules: {}, -}; - -class Validator { - eventMap = { - onBlur: ['focusout'], - onChange: ['change', 'input'], - }; - - /** - * constructor - * @param {HTMLFormElement} form - The form element to validate - * @param {import("./types").ValidatorOptions} option - The options for the validator - */ - constructor(form, option = {}) { - this.form = form; - this.config = { ...defaultOptions, ...option }; - this.rules = { ...rules, ...this.config.customRules }; - this.eventRegistry = new Map(); - this.invalidFields = new Set(); - this.handleChange = this.handleChange.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - this.handleRevalidate = this.handleRevalidate.bind(this); - this.register(); - } - - /** - * register event listener - * @returns {void} - */ - register() { - if (this.config.shouldValidate !== false) { - const eventNames = this.eventMap[this.config.shouldValidate]; - eventNames.forEach((eventName) => { - document.addEventListener(eventName, this.handleChange); - this.eventRegistry.set(document, [ - ...(this.eventRegistry.get(document) || []), - { type: eventName, listener: this.handleChange }, - ]); - }); - } - // 失敗したフィールドの再バリデーション用のイベントリスナーを登録 - if (this.config.shouldRevalidate !== false) { - const revalidateEventNames = this.eventMap[this.config.shouldRevalidate]; - revalidateEventNames.forEach((eventName) => { - document.addEventListener(eventName, this.handleRevalidate); - this.eventRegistry.set(document, [ - ...(this.eventRegistry.get(document) || []), - { type: eventName, listener: this.handleRevalidate }, - ]); - }); - } - if (this.config.shouldValidateOnSubmit) { - this.form.addEventListener('submit', this.handleSubmit); - this.eventRegistry.set(this.form, [{ type: 'submit', listener: this.handleSubmit }]); - } - } - - /** - * unregister event listener - * @returns {void} - */ - unregister() { - for (const [element, listeners] of this.eventRegistry) { - listeners.forEach(({ type, listener }) => { - element.removeEventListener(type, listener); - }); - } - this.eventRegistry.clear(); - } - - /** - * バリデーションルールを登録する - * @param {string} name - ルールの名前 - * @param {import("./types").ValidationRule} rule - ルールの関数 - * @returns {void} - */ - registerRule(name, rule) { - this.rules[name] = rule; - } - - /** - * バリデーションルールを削除する - * @param {string} name - ルールの名前 - * @returns {void} - */ - unregisterRule(name) { - delete this.rules[name]; - } - - /** - * destroy - * @returns {void} - */ - destroy() { - this.unregister(); - } - - /** - * handleChange - * @param {Event} event - The event to handle - * @returns {void} - */ - handleChange(event) { - if ( - !(event.target instanceof HTMLInputElement) && - !(event.target instanceof HTMLSelectElement) && - !(event.target instanceof HTMLTextAreaElement) - ) { - return; - } - if (this.config.shouldValidate === false) { - return; - } - - if (!this.getFields().includes(event.target)) { - return; - } - this.checkValidity(event.target); - } - - /** - * handleRevalidate - * @param {Event} event - The event to handle - * @returns {void} - */ - handleRevalidate(event) { - if ( - !(event.target instanceof HTMLInputElement) && - !(event.target instanceof HTMLSelectElement) && - !(event.target instanceof HTMLTextAreaElement) - ) { - return; - } - - if (this.config.shouldRevalidate === false) { - return; - } - - if (!this.getFields().includes(event.target)) { - return; - } - - // 失敗したフィールドの場合のみ再バリデーション - if (!this.invalidFields.has(event.target.name)) { - return; - } - this.checkValidity(event.target); - } - - /** - * handleSubmit - * @param {Event} event - The event to handle - * @returns {void} - */ - handleSubmit(event) { - if (!this.config.shouldValidateOnSubmit) { - return; - } - if (!this.checkFormValidity()) { - event.preventDefault(); - - if (this.config.shouldScrollOnSubmitFailed) { - this.scrollToInvalidElement().then(() => { - if (this.config.shouldFocusOnSubmitFailed) { - this.focusInvalidElement(); - } - }); - } - if (!this.config.shouldScrollOnSubmitFailed && this.config.shouldFocusOnSubmitFailed) { - this.focusInvalidElement(); - } - } - } - - /** - * フォーム要素のすべての検証を行う - * @returns {ok: boolean; validationResults: import("./types").ValidationResult[]} - 検証結果 - */ - validateAll() { - const fields = this.getFields().filter((field) => field.disabled === false); // disabledな要素はバリデーション対象外 - const names = [...new Set(fields.map((field) => field.name.replace(/\[[\d]*\]/, '')))]; - const nameToValidation = names.reduce((obj, name) => { - const validations = this.extractValidations(name); - if (validations.length === 0) { - return obj; - } - return { ...obj, [name]: validations }; - }, {}); - const allValidations = Object.values(nameToValidation) - .flat() - .filter((validation) => validation.rule.substring(0, 4) === 'all_'); - const validationResults = []; - allValidations.forEach((validation) => { - const checkboxes = Array.from(fields).filter( - (field) => field.name === validation.field || field.name === `${validation.field}[]` - ); - const ok = this.rules[validation.rule]('', validation.arg, checkboxes, validation); - this.label(validation.id, -1, ok); - this.toggleClass(validation.field, ok); - validationResults.push({ - ok, - element: checkboxes, - validation, - }); - }); - fields.forEach((field) => { - const validations = nameToValidation[field.name.replace(/\[[\d]*\]/, '')]; - if (typeof validations === 'undefined') { - return; - } - const { ok, results } = this.validate(field, validations); - this.toggleClass(field.name, ok); - validationResults.push(...results); - }); - const ok = validationResults.every((result) => result.ok); - return { - ok, - validationResults, - }; - } - - /** - * フォーム要素の検証を行う - * @param {HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement} input - The input element to validate - * @param {import("./types").Validation[]} validations - The validations to validate - * @returns {ok: boolean; results: import("./types").ValidationResult[]} - 検証結果 - */ - validate(input, validations) { - const matches = input.name.match(/\[([\d]+)\]/); - const number = matches !== null ? parseInt(matches[1], 10) : -1; - const results = []; - if (validations.length > 0) { - validations.forEach((validation) => { - if (!validation || !validation.rule) { - return; - } - if (!isFunction(this.rules[validation.rule])) { - return; - } - if (validation.rule.substring(0, 4) === 'all_') { - return; - } - if ( - input instanceof HTMLInputElement && - (input.type === 'checkbox' || input.type === 'radio') && - validation.rule !== 'required' - ) { - return; - } - const ok = this.rules[validation.rule](input.value, validation.arg, input, validation); - results.push({ - ok, - element: input, - validation, - }); - this.label(validation.id, number, ok); - }); - } - const ok = results.every((result) => result.ok); - return { - ok, - results, - }; - } - - /** - * フォームの検証を行う - * @returns {boolean} - 検証結果 - */ - checkFormValidity() { - const { ok, validationResults } = this.validateAll(); - validationResults.forEach((result) => { - if (Array.isArray(result.element)) { - result.element.forEach((element) => { - this.toggleAriaInvalid(element, result.ok); - // 失敗フィールドを管理 - if (!result.ok) { - this.invalidFields.add(element.name); - } else { - this.invalidFields.delete(element.name); - } - }); - } else { - this.toggleAriaInvalid(result.element, result.ok); - // 失敗フィールドを管理 - if (!result.ok) { - this.invalidFields.add(result.element.name); - } else { - this.invalidFields.delete(result.element.name); - } - } - }); - if (!ok) { - this.config.onSubmitFailed(validationResults, this.form); - } - this.config.onValidated(validationResults, this.form); - return ok; - } - - /** - * フォーム要素の検証を行う - * @param {HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement} input - The input element to validate - * @returns {boolean} - 検証結果 - */ - checkValidity(input) { - const validations = this.extractValidations(input.name); - const { ok, results } = this.validate(input, validations); - this.toggleClass(input.name, ok); - this.toggleAriaInvalid(input, ok); - if (ok) { - this.invalidFields.delete(input.name); - this.config.onValid(results, input); - } else { - this.invalidFields.add(input.name); - this.config.onInvalid(results, input); - } - this.config.onValidated(results, input); - return ok; - } - - /** - * name属性に対するバリデーションを抽出する - * @param {string} name - The name of the input element to validate - * @returns {import("./types").Validation[]} - The validations - */ - extractValidations(name) { - const replaced = name.replace(/\[[\d]*\]/, ''); - const inputs = Array.from(this.form.elements) - .filter( - (element) => element.name.startsWith(`${replaced}:validator#`) || element.name.startsWith(`${replaced}:v#`) - ) - .filter((element) => !element.disabled); - return inputs.reduce((validations, input) => { - const element = input; - const matchs = element.name.match(/^(.*):(validator|v)#(.*)$/); - if (matchs === null) { - return validations; - } - - return [ - ...validations, - { - field: matchs[1], - rule: matchs[3], - arg: element.value, - id: element.id, - name: element.name, - }, - ]; - }, []); - } - - /** - * フォームのフィールドを取得する - * FieldSetElementはバリデーション対象外 - * @returns {(HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement | HTMLButtonElement)[]} - The fields - */ - getFields() { - const fields = getFormElements(this.form); - return fields; - } - - /** - * ラベル要素を更新する - * @param {string} id - ラベル要素のid - * @param {number} number - ラベル要素の番号 - * @param {boolean} ok - ラベル要素の有効性 - * @returns {void} - */ - label(id, number, ok) { - const { resultClassName } = this.config; - const label = document.querySelectorAll(`[data-validator-label="${id}"]`); - let target = null; - if (label.length < 1) { - target = document.querySelector(`label[for="${id}"]`); - } - if (label.length === 1) { - target = document.querySelector(`[data-validator-label="${id}"]`); - } - if (label.length > 1 && number > -1) { - target = label[number]; - } - if (target) { - target.classList.remove(resultClassName); - target.classList.remove(`${resultClassName}0`); - target.classList.remove(`${resultClassName}1`); - target.classList.add(`${resultClassName}${ok ? '1' : '0'}`); - } - } - - /** - * クラスを切り替える - * @param {string} name - クラスを切り替える要素のname属性 - * @param {boolean} ok - クラスを切り替える要素の有効性 - * @returns {void} - */ - toggleClass(name, ok) { - const { okClassName, ngClassName } = this.config; - const fields = this.getFields(); - const elements = fields - .filter((field) => field.name === name || field.name === `${name}[]`) - .concat(Array.from(document.querySelectorAll(`[data-validator="${name.replace(/\[[\d]*\]/, '')}"]`))); - if (elements.length > 0) { - elements.forEach((element) => { - if (ok) { - element.classList.remove(ngClassName); - element.classList.add(okClassName); - } else { - element.classList.remove(okClassName); - element.classList.add(ngClassName); - } - }); - } - } - - /** - * aria-invalid属性を切り替える - * @param {HTMLElement} element - The element to toggle aria-invalid - * @param {boolean} ok - The validity of the element - * @returns {void} - */ - toggleAriaInvalid(element, ok) { - if ( - element instanceof HTMLInputElement || - element instanceof HTMLSelectElement || - element instanceof HTMLTextAreaElement - ) { - element.setAttribute('aria-invalid', ok ? 'false' : 'true'); - } - } - - /** - * invalidな要素の内、最初の要素を返す - * @returns {HTMLElement | null} - The invalid element - */ - getInvalidElement() { - const { ngClassName } = this.config; - const fields = this.getFields(); - return fields.find((field) => field.classList.contains(ngClassName)) || null; - } - - /** - * 無効な要素までスクロールする - * @returns {Promise} - */ - scrollToInvalidElement() { - return new Promise((resolve) => { - const element = this.getInvalidElement(); - if (element) { - element.scrollIntoView({ behavior: 'smooth' }); - let isScrolling; - - const onScroll = () => { - clearTimeout(isScrolling); - isScrolling = setTimeout(() => { - window.removeEventListener('scroll', onScroll); - resolve(); - }, 100); // 100ms スクロールが停止したらスクロール終了と判断 - }; - - window.addEventListener('scroll', onScroll); - } - }); - } - - /** - * 無効な要素をフォーカスする - * @returns {void} - */ - focusInvalidElement() { - const element = this.getInvalidElement(); - if (element) { - element.focus(); - } - } -} - -export default Validator;