@geckou/ui-core 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/date.d.ts +4 -1
- package/dist/date.js +44 -14
- package/dist/form-validation-store.js +11 -6
- package/dist/text.js +1 -1
- package/dist/validation.js +6 -4
- package/package.json +5 -3
package/dist/date.d.ts
CHANGED
|
@@ -12,7 +12,10 @@ export declare function daysInMonth(year: number, month: number): number;
|
|
|
12
12
|
export declare function formatDateValue(value: string, type?: DateType): string;
|
|
13
13
|
/** YYYY-MM-DD を年・月・日へ分解する。欠けている要素は空文字 */
|
|
14
14
|
export declare function splitDate(value: string): DateObject;
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* 年・月・日から日付文字列を組み立てる。要素が欠けていれば空文字。
|
|
17
|
+
* 月・日は 2 桁へゼロ埋めする('2024-1-5' ではなく '2024-01-05')
|
|
18
|
+
*/
|
|
16
19
|
export declare function composeDateValue(dateObject: DateObject, type?: DateType): string;
|
|
17
20
|
/**
|
|
18
21
|
* 年・月・日の入力内容を検証する。
|
package/dist/date.js
CHANGED
|
@@ -7,7 +7,11 @@ function isNumeric(value) {
|
|
|
7
7
|
}
|
|
8
8
|
/** 指定した年月の日数。month は 1 始まり */
|
|
9
9
|
export function daysInMonth(year, month) {
|
|
10
|
-
|
|
10
|
+
// new Date(year, ...) は年 0〜99 を 1900 年代として扱う(0 → 1900)。
|
|
11
|
+
// setFullYear なら西暦そのままで解釈される
|
|
12
|
+
const date = new Date(0);
|
|
13
|
+
date.setFullYear(year, month, 0);
|
|
14
|
+
return date.getDate();
|
|
11
15
|
}
|
|
12
16
|
/**
|
|
13
17
|
* 任意の日付文字列を YYYY-MM-DD(type='month' なら YYYY-MM)へ正規化する。
|
|
@@ -18,24 +22,43 @@ export function daysInMonth(year, month) {
|
|
|
18
22
|
* getFullYear / getMonth / getDate を使って変換する。
|
|
19
23
|
*/
|
|
20
24
|
export function formatDateValue(value, type = 'date') {
|
|
21
|
-
if (!value)
|
|
25
|
+
if (!value) {
|
|
22
26
|
return '';
|
|
23
|
-
|
|
27
|
+
}
|
|
28
|
+
// 末尾を固定する。固定しないと '2024-01-15abc' のような値の頭だけを拾ってしまう。
|
|
29
|
+
// ISO 8601('2024-01-14T23:00:00.000Z')もここでは一致せず、下の Date へ回して
|
|
30
|
+
// ローカル時刻の日付に直す(字面の先頭 10 文字を採ると UTC 日付になり、
|
|
31
|
+
// JST では前日にずれる)
|
|
32
|
+
const matched = value.match(/^(\d{4})-(\d{1,2})(?:-(\d{1,2}))?$/);
|
|
24
33
|
if (matched) {
|
|
25
34
|
const [, year, month, day] = matched;
|
|
26
|
-
const
|
|
27
|
-
if (
|
|
35
|
+
const monthNumber = Number(month);
|
|
36
|
+
if (monthNumber < 1 || monthNumber > 12) {
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
const yearMonth = `${year}-${pad(monthNumber)}`;
|
|
40
|
+
if (type === 'month') {
|
|
28
41
|
return yearMonth;
|
|
42
|
+
}
|
|
29
43
|
// type='date' は完全な日付を要求する。日が欠けていれば入力欄には反映しない
|
|
30
|
-
|
|
44
|
+
if (!day) {
|
|
45
|
+
return '';
|
|
46
|
+
}
|
|
47
|
+
const dayNumber = Number(day);
|
|
48
|
+
if (dayNumber < 1 || dayNumber > daysInMonth(Number(year), monthNumber)) {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
return `${yearMonth}-${pad(dayNumber)}`;
|
|
31
52
|
}
|
|
32
53
|
const parsed = new Date(value);
|
|
33
54
|
// 不正な日付文字列で toISOString() が throw していたため、先に弾く
|
|
34
|
-
if (Number.isNaN(parsed.getTime()))
|
|
55
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
35
56
|
return '';
|
|
57
|
+
}
|
|
36
58
|
const yearMonth = `${parsed.getFullYear()}-${pad(parsed.getMonth() + 1)}`;
|
|
37
|
-
if (type === 'month')
|
|
59
|
+
if (type === 'month') {
|
|
38
60
|
return yearMonth;
|
|
61
|
+
}
|
|
39
62
|
return `${yearMonth}-${pad(parsed.getDate())}`;
|
|
40
63
|
}
|
|
41
64
|
/** YYYY-MM-DD を年・月・日へ分解する。欠けている要素は空文字 */
|
|
@@ -43,13 +66,18 @@ export function splitDate(value) {
|
|
|
43
66
|
const [year = '', month = '', day = ''] = value.split('-');
|
|
44
67
|
return { year, month, day };
|
|
45
68
|
}
|
|
46
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* 年・月・日から日付文字列を組み立てる。要素が欠けていれば空文字。
|
|
71
|
+
* 月・日は 2 桁へゼロ埋めする('2024-1-5' ではなく '2024-01-05')
|
|
72
|
+
*/
|
|
47
73
|
export function composeDateValue(dateObject, type = 'date') {
|
|
48
74
|
const { year, month, day } = dateObject;
|
|
49
75
|
const parts = type === 'month' ? [year, month] : [year, month, day];
|
|
50
|
-
if (parts.some(part => !part))
|
|
76
|
+
if (parts.some((part) => !part)) {
|
|
51
77
|
return '';
|
|
52
|
-
|
|
78
|
+
}
|
|
79
|
+
const [yearPart, ...rest] = parts;
|
|
80
|
+
return [yearPart, ...rest.map((part) => pad(Number(part)))].join('-');
|
|
53
81
|
}
|
|
54
82
|
/**
|
|
55
83
|
* 年・月・日の入力内容を検証する。
|
|
@@ -61,10 +89,11 @@ export function validateDateObject(dateObject, options = {}) {
|
|
|
61
89
|
const day = type === 'month' ? '' : dateObject.day;
|
|
62
90
|
const valid = { isValid: true, message: '' };
|
|
63
91
|
// 全部空で必須でなければ未入力として通す
|
|
64
|
-
if ([year, month, day].every(value => !value) && !isRequired)
|
|
92
|
+
if ([year, month, day].every((value) => !value) && !isRequired) {
|
|
65
93
|
return valid;
|
|
94
|
+
}
|
|
66
95
|
const requiredValues = type === 'month' ? [year, month] : [year, month, day];
|
|
67
|
-
if (requiredValues.some(value => isRequired && !value)) {
|
|
96
|
+
if (requiredValues.some((value) => isRequired && !value)) {
|
|
68
97
|
return { isValid: false, message: MESSAGES.required };
|
|
69
98
|
}
|
|
70
99
|
if (year.length !== 4 || !isNumeric(year)) {
|
|
@@ -80,9 +109,10 @@ export function validateDateObject(dateObject, options = {}) {
|
|
|
80
109
|
if (monthNumber < 1 || monthNumber > 12) {
|
|
81
110
|
return { isValid: false, message: MESSAGES.monthOutOfRange };
|
|
82
111
|
}
|
|
112
|
+
// parseInt('00') は 0(falsy)。null との比較にしないと日 '00' の検査が飛ぶ
|
|
83
113
|
const dayNumber = day ? parseInt(day, 10) : null;
|
|
84
114
|
const maxDay = daysInMonth(parseInt(year, 10), monthNumber);
|
|
85
|
-
if (dayNumber && (dayNumber < 1 || dayNumber > maxDay)) {
|
|
115
|
+
if (dayNumber !== null && (dayNumber < 1 || dayNumber > maxDay)) {
|
|
86
116
|
return { isValid: false, message: MESSAGES.dayOutOfRange(maxDay) };
|
|
87
117
|
}
|
|
88
118
|
return valid;
|
|
@@ -18,8 +18,9 @@ export function createFormValidationStore() {
|
|
|
18
18
|
const buildSnapshot = () => {
|
|
19
19
|
const invalidNames = [];
|
|
20
20
|
states.forEach((isValid, name) => {
|
|
21
|
-
if (!isValid)
|
|
21
|
+
if (!isValid) {
|
|
22
22
|
invalidNames.push(name);
|
|
23
|
+
}
|
|
23
24
|
});
|
|
24
25
|
return { isAllValid: invalidNames.length === 0, invalidNames };
|
|
25
26
|
};
|
|
@@ -29,15 +30,17 @@ export function createFormValidationStore() {
|
|
|
29
30
|
const isSame = next.isAllValid === snapshot.isAllValid &&
|
|
30
31
|
next.invalidNames.length === snapshot.invalidNames.length &&
|
|
31
32
|
next.invalidNames.every((name, index) => name === snapshot.invalidNames[index]);
|
|
32
|
-
if (isSame)
|
|
33
|
+
if (isSame) {
|
|
33
34
|
return;
|
|
35
|
+
}
|
|
34
36
|
snapshot = next;
|
|
35
|
-
listeners.forEach(listener => listener());
|
|
37
|
+
listeners.forEach((listener) => listener());
|
|
36
38
|
};
|
|
37
39
|
return {
|
|
38
40
|
setValid(name, isValid) {
|
|
39
|
-
if (states.get(name) === isValid)
|
|
41
|
+
if (states.get(name) === isValid) {
|
|
40
42
|
return;
|
|
43
|
+
}
|
|
41
44
|
states.set(name, isValid);
|
|
42
45
|
notify();
|
|
43
46
|
},
|
|
@@ -45,13 +48,15 @@ export function createFormValidationStore() {
|
|
|
45
48
|
return states.get(name) ?? true;
|
|
46
49
|
},
|
|
47
50
|
remove(name) {
|
|
48
|
-
if (!states.delete(name))
|
|
51
|
+
if (!states.delete(name)) {
|
|
49
52
|
return;
|
|
53
|
+
}
|
|
50
54
|
notify();
|
|
51
55
|
},
|
|
52
56
|
reset() {
|
|
53
|
-
if (states.size === 0)
|
|
57
|
+
if (states.size === 0) {
|
|
54
58
|
return;
|
|
59
|
+
}
|
|
55
60
|
states.clear();
|
|
56
61
|
notify();
|
|
57
62
|
},
|
package/dist/text.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const FULL_WIDTH_ALPHANUMERIC = /[A-Za-z0-9]/g;
|
|
2
2
|
/** 全角の英数字を半角へ変換する。全角以外の文字はそのまま残す */
|
|
3
3
|
export function convertFullWidthToHalfWidth(value) {
|
|
4
|
-
return value.replace(FULL_WIDTH_ALPHANUMERIC, character => String.fromCharCode(character.charCodeAt(0) - 0xfee0));
|
|
4
|
+
return value.replace(FULL_WIDTH_ALPHANUMERIC, (character) => String.fromCharCode(character.charCodeAt(0) - 0xfee0));
|
|
5
5
|
}
|
package/dist/validation.js
CHANGED
|
@@ -17,14 +17,15 @@ export function isEmptyValue(value) {
|
|
|
17
17
|
* 新規インスタンスは lastIndex = 0 なので、sticky の意味を保ったまま結果が安定する。
|
|
18
18
|
*/
|
|
19
19
|
export function runValidates(value, validates) {
|
|
20
|
-
if (isEmptyValue(value))
|
|
20
|
+
if (isEmptyValue(value)) {
|
|
21
21
|
return [];
|
|
22
|
+
}
|
|
22
23
|
return validates
|
|
23
|
-
.filter(validate => {
|
|
24
|
+
.filter((validate) => {
|
|
24
25
|
const regex = new RegExp(validate.regex.source, validate.regex.flags);
|
|
25
26
|
return !regex.test(String(value));
|
|
26
27
|
})
|
|
27
|
-
.map(validate => validate.message);
|
|
28
|
+
.map((validate) => validate.message);
|
|
28
29
|
}
|
|
29
30
|
/**
|
|
30
31
|
* 必須チェックと validates をまとめて適用する。
|
|
@@ -32,7 +33,8 @@ export function runValidates(value, validates) {
|
|
|
32
33
|
*/
|
|
33
34
|
export function validateInputValue(value, options = {}) {
|
|
34
35
|
const { isRequired = false, validates = [] } = options;
|
|
35
|
-
if (isEmptyValue(value))
|
|
36
|
+
if (isEmptyValue(value)) {
|
|
36
37
|
return isRequired ? [MESSAGES.required] : [];
|
|
38
|
+
}
|
|
37
39
|
return runValidates(value, validates);
|
|
38
40
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geckou/ui-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Framework-agnostic logic shared by @geckou/ui-vue and @geckou/ui-react",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -34,10 +34,12 @@
|
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
37
|
-
"
|
|
37
|
+
"lint": "eslint src",
|
|
38
|
+
"lint:fix": "eslint src --fix",
|
|
39
|
+
"prepublishOnly": "yarn build",
|
|
38
40
|
"test": "vitest run",
|
|
39
41
|
"test:watch": "vitest",
|
|
40
|
-
"
|
|
42
|
+
"type-check": "tsc --noEmit"
|
|
41
43
|
},
|
|
42
44
|
"devDependencies": {
|
|
43
45
|
"typescript": "^5.5.3",
|