@t007/input 0.0.22 → 0.0.24
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/README.md +64 -2
- package/dist/index.css +6 -4
- package/dist/index.d.ts +20 -51
- package/dist/index.global.js +142 -156
- package/dist/index.js +97 -125
- package/dist/react.d.ts +139 -0
- package/dist/react.js +255 -0
- package/package.json +22 -2
package/dist/index.js
CHANGED
|
@@ -1,58 +1,74 @@
|
|
|
1
|
-
// src/index.js
|
|
2
|
-
import { isArr, isStr, createEl, loadResource,
|
|
1
|
+
// src/js/index.js
|
|
2
|
+
import { isArr, isStr, createEl, loadResource, formatSize as formatFileSize } from "@t007/utils";
|
|
3
|
+
import { initScrollAssist } from "@t007/utils/hooks/vanilla";
|
|
4
|
+
|
|
5
|
+
// src/ts/utils/consts.ts
|
|
6
|
+
var violationKeys = ["valueMissing", "typeMismatch", "patternMismatch", "stepMismatch", "tooShort", "tooLong", "rangeUnderflow", "rangeOverflow", "badInput", "customError"];
|
|
7
|
+
var dateTypes = ["date", "time", "datetime-local", "month"];
|
|
8
|
+
var nativeIconTypes = [...dateTypes];
|
|
9
|
+
|
|
10
|
+
// src/ts/utils/fn.ts
|
|
11
|
+
import { formatSize } from "@t007/utils";
|
|
12
|
+
function getStrengthLevel(value, minLength = 0) {
|
|
13
|
+
value = value.trim();
|
|
14
|
+
let level = 0;
|
|
15
|
+
if (value.length < minLength) level = 1;
|
|
16
|
+
else {
|
|
17
|
+
if (/[a-z]/.test(value)) level++;
|
|
18
|
+
if (/[A-Z]/.test(value)) level++;
|
|
19
|
+
if (/[0-9]/.test(value)) level++;
|
|
20
|
+
if (/[\W_]/.test(value)) level++;
|
|
21
|
+
}
|
|
22
|
+
return Math.min(level, 4);
|
|
23
|
+
}
|
|
24
|
+
function getFilesHelper(files, opts) {
|
|
25
|
+
if (!files || !files.length) return { violation: null, message: "" };
|
|
26
|
+
const totalFiles = files.length;
|
|
27
|
+
let totalSize = 0, currFiles = 0;
|
|
28
|
+
const setMaxError = (size, max, n = 0) => ({ violation: "rangeOverflow", message: n ? `File ${files.length > 1 ? n : ""} size of ${formatSize(size)} exceeds the per file maximum of ${formatSize(max)}` : `Total files size of ${formatSize(size)} exceeds the total maximum of ${formatSize(max)}` });
|
|
29
|
+
const setMinError = (size, min, n = 0) => ({ violation: "rangeUnderflow", message: n ? `File ${files.length > 1 ? n : ""} size of ${formatSize(size)} is less than the per file minimum of ${formatSize(min)}` : `Total files size of ${formatSize(size)} is less than the total minimum of ${formatSize(min)}` });
|
|
30
|
+
for (const file of files) {
|
|
31
|
+
currFiles++;
|
|
32
|
+
totalSize += file.size;
|
|
33
|
+
if (opts.accept) {
|
|
34
|
+
const acceptedTypes = opts.accept.split(",").map((type) => type.trim().replace(/^[*\.]+|[*\.]+$/g, "")).filter(Boolean) || [];
|
|
35
|
+
if (!acceptedTypes.some((type) => file.type.includes(type))) return { violation: "typeMismatch", message: `File${currFiles > 1 ? currFiles : ""} type of '${file.type}' is not accepted.` };
|
|
36
|
+
}
|
|
37
|
+
if (opts.maxSize && file.size > opts.maxSize) return setMaxError(file.size, opts.maxSize, currFiles);
|
|
38
|
+
if (opts.minSize && file.size < opts.minSize) return setMinError(file.size, opts.minSize, currFiles);
|
|
39
|
+
if (opts.multiple) {
|
|
40
|
+
if (opts.maxTotalSize && totalSize > opts.maxTotalSize) return setMaxError(totalSize, opts.maxTotalSize);
|
|
41
|
+
if (opts.minTotalSize && totalSize < opts.minTotalSize) return setMinError(totalSize, opts.minTotalSize);
|
|
42
|
+
if (opts.maxLength && totalFiles > opts.maxLength) return { violation: "tooLong", message: `Selected ${totalFiles} files exceeds the maximum of ${opts.maxLength} allowed file${opts.maxLength == 1 ? "" : "s"}` };
|
|
43
|
+
if (opts.minLength && totalFiles < opts.minLength) return { violation: "tooShort", message: `Selected ${totalFiles} files is less than the minimum of ${opts.minLength} allowed file${opts.minLength == 1 ? "" : "s"}` };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { violation: null, message: "" };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/js/index.js
|
|
3
50
|
var formManager = {
|
|
4
51
|
forms: document.getElementsByClassName("t007-input-form"),
|
|
5
|
-
violationKeys
|
|
52
|
+
violationKeys,
|
|
6
53
|
init() {
|
|
7
|
-
t007.FM.observeDOMForFields();
|
|
8
|
-
Array.from(t007.FM.forms).forEach(t007.FM.handleFormValidation);
|
|
54
|
+
t007.FM.observeDOMForFields(), Array.prototype.forEach.call(t007.FM.forms, t007.FM.handleFormValidation);
|
|
9
55
|
},
|
|
10
56
|
observeDOMForFields() {
|
|
11
57
|
new MutationObserver((mutations) => {
|
|
12
|
-
for (const mutation of mutations)
|
|
58
|
+
for (const mutation of mutations)
|
|
13
59
|
for (const node of mutation.addedNodes) {
|
|
14
|
-
if (!node.tagName || !(node
|
|
60
|
+
if (!node.tagName || !(node.classList.contains("t007-input-field") || node.querySelector(".t007-input-field"))) continue;
|
|
15
61
|
for (const field2 of [...node.querySelector(".t007-input-field") ? node.querySelectorAll(".t007-input-field") : [node]]) t007.FM.setUpField(field2);
|
|
16
62
|
}
|
|
17
|
-
}
|
|
18
63
|
}).observe(document.body, { childList: true, subtree: true });
|
|
19
64
|
},
|
|
20
|
-
getFilesHelper(files, opts)
|
|
21
|
-
|
|
22
|
-
const totalFiles = files.length;
|
|
23
|
-
let totalSize = 0;
|
|
24
|
-
let currFiles = 0;
|
|
25
|
-
const setMaxError = (size, max, n = 0) => ({ violation: "rangeOverflow", message: n ? `File ${files.length > 1 ? n : ""} size of ${t007.FM.formatSize(size)} exceeds the per file maximum of ${t007.FM.formatSize(max)}` : `Total files size of ${t007.FM.formatSize(size)} exceeds the total maximum of ${t007.FM.formatSize(max)}` });
|
|
26
|
-
const setMinError = (size, min, n = 0) => ({ violation: "rangeUnderflow", message: n ? `File ${files.length > 1 ? n : ""} size of ${t007.FM.formatSize(size)} is less than the per file minimum of ${t007.FM.formatSize(min)}` : `Total files size of ${t007.FM.formatSize(size)} is less than the total minimum of ${t007.FM.formatSize(min)}` });
|
|
27
|
-
for (const file of files) {
|
|
28
|
-
currFiles++;
|
|
29
|
-
totalSize += file.size;
|
|
30
|
-
if (opts.accept) {
|
|
31
|
-
const acceptedTypes = opts.accept.split(",").map((type) => type.trim().replace(/^[*\.]+|[*\.]+$/g, "")).filter(Boolean) || [];
|
|
32
|
-
if (!acceptedTypes.some((type) => file.type.includes(type))) return { violation: "typeMismatch", message: `File${currFiles > 1 ? currFiles : ""} type of '${file.type}' is not accepted.` };
|
|
33
|
-
}
|
|
34
|
-
if (opts.maxSize && file.size > opts.maxSize) return setMaxError(file.size, opts.maxSize, currFiles);
|
|
35
|
-
if (opts.minSize && file.size < opts.minSize) return setMinError(file.size, opts.minSize, currFiles);
|
|
36
|
-
if (opts.multiple) {
|
|
37
|
-
if (opts.maxTotalSize && totalSize > opts.maxTotalSize) return setMaxError(totalSize, opts.maxTotalSize);
|
|
38
|
-
if (opts.minTotalSize && totalSize < opts.minTotalSize) return setMinError(totalSize, opts.minTotalSize);
|
|
39
|
-
if (opts.maxLength && totalFiles > opts.maxLength) return { violation: "tooLong", message: `Selected ${totalFiles} files exceeds the maximum of ${opts.maxLength} allowed file${opts.maxLength == 1 ? "" : "s"}` };
|
|
40
|
-
if (opts.minLength && totalFiles < opts.minLength) return { violation: "tooShort", message: `Selected ${totalFiles} files is less than the minimum of ${opts.minLength} allowed file${opts.minLength == 1 ? "" : "s"}` };
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return { violation: null, message: "" };
|
|
44
|
-
},
|
|
45
|
-
formatSize(size, decimals = 3, base = 1e3) {
|
|
46
|
-
if (size < base) return `${size} byte${size == 1 ? "" : "s"}`;
|
|
47
|
-
const units = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"], exponent = Math.min(Math.floor(Math.log(size) / Math.log(base)), units.length - 1);
|
|
48
|
-
return `${(size / Math.pow(base, exponent)).toFixed(decimals).replace(/\.0+$/, "")} ${units[exponent]}`;
|
|
49
|
-
},
|
|
65
|
+
getFilesHelper: (files, opts) => getFilesHelper(files, opts),
|
|
66
|
+
formatSize: (size, decimals = 3, base = 1e3) => formatFileSize(size, decimals, base),
|
|
50
67
|
togglePasswordType: (input) => input.type = input.type === "password" ? "text" : "password",
|
|
51
68
|
toggleFilled: (input) => input?.toggleAttribute("data-filled", input.type === "checkbox" || input.type === "radio" ? input.checked : input.value !== "" || input.files?.length > 0),
|
|
52
69
|
setFallbackHelper(field2) {
|
|
53
70
|
const helperTextWrapper = field2?.querySelector(".t007-input-helper-text-wrapper");
|
|
54
|
-
|
|
55
|
-
helperTextWrapper.append(createEl("p", { className: "t007-input-helper-text" }, { violation: "auto" }));
|
|
71
|
+
!helperTextWrapper?.querySelector(".t007-input-helper-text[data-violation='auto']") && helperTextWrapper?.append(createEl("p", { className: "t007-input-helper-text" }, { violation: "auto" }));
|
|
56
72
|
},
|
|
57
73
|
setFieldListeners(field2) {
|
|
58
74
|
if (!field2) return;
|
|
@@ -60,47 +76,31 @@ var formManager = {
|
|
|
60
76
|
if (input.type === "file")
|
|
61
77
|
input.addEventListener("input", async () => {
|
|
62
78
|
const file = input.files?.[0], img = new Image();
|
|
63
|
-
img.onload = () => {
|
|
64
|
-
|
|
65
|
-
input.classList.add("t007-input-image-selected");
|
|
66
|
-
setTimeout(() => URL.revokeObjectURL(src), 1e3);
|
|
67
|
-
};
|
|
68
|
-
img.onerror = () => {
|
|
69
|
-
input.style.removeProperty("--t007-input-image-src");
|
|
70
|
-
input.classList.remove("t007-input-image-selected");
|
|
71
|
-
URL.revokeObjectURL(src);
|
|
72
|
-
};
|
|
79
|
+
img.onload = () => (input.style.setProperty("--t007-input-image-src", `url(${src})`), input.classList.add("t007-input-image-selected"), setTimeout(() => URL.revokeObjectURL(src), 1e3));
|
|
80
|
+
img.onerror = () => (input.style.removeProperty("--t007-input-image-src"), input.classList.remove("t007-input-image-selected"), URL.revokeObjectURL(src));
|
|
73
81
|
let src;
|
|
74
82
|
if (file?.type?.startsWith("image")) src = URL.createObjectURL(file);
|
|
75
|
-
else if (file?.type?.startsWith("video"))
|
|
83
|
+
else if (file?.type?.startsWith("video"))
|
|
76
84
|
src = await new Promise((resolve) => {
|
|
77
85
|
let video = createEl("video"), canvas = createEl("canvas"), context = canvas.getContext("2d");
|
|
78
86
|
video.ontimeupdate = () => {
|
|
79
87
|
context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
|
|
80
|
-
canvas.toBlob((blob) => resolve(URL.createObjectURL(blob)));
|
|
81
|
-
URL.revokeObjectURL(video.src);
|
|
88
|
+
canvas.toBlob((blob) => resolve(URL.createObjectURL(blob))), URL.revokeObjectURL(video.src);
|
|
82
89
|
video = video.src = video.onloadedmetadata = video.ontimeupdate = null;
|
|
83
90
|
};
|
|
84
91
|
video.onloadeddata = () => video.currentTime = 3;
|
|
85
92
|
video.src = URL.createObjectURL(file);
|
|
86
93
|
});
|
|
87
|
-
|
|
88
|
-
if (!src) {
|
|
89
|
-
input.style.removeProperty("--t007-input-image-src");
|
|
90
|
-
input.classList.remove("t007-input-image-selected");
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
94
|
+
if (!src) return input.style.removeProperty("--t007-input-image-src"), input.classList.remove("t007-input-image-selected");
|
|
93
95
|
img.src = src;
|
|
94
96
|
});
|
|
95
97
|
if (floatingLabel) floatingLabel.ontransitionend = () => floatingLabel.classList.remove("t007-input-shake");
|
|
96
98
|
if (eyeOpen && eyeClosed) eyeOpen.onclick = eyeClosed.onclick = () => t007.FM.togglePasswordType(input);
|
|
97
|
-
initScrollAssist(field2.querySelector(".t007-input-helper-text-wrapper"), { vertical: false });
|
|
99
|
+
initScrollAssist(field2.querySelector(".t007-input-helper-text-wrapper"), { vertical: false, assistClassName: "t007-input-scroll-assist" });
|
|
98
100
|
},
|
|
99
101
|
setUpField(field2) {
|
|
100
102
|
if (field2.dataset.setUp) return;
|
|
101
|
-
t007.FM.toggleFilled(field2.querySelector(".t007-input"));
|
|
102
|
-
t007.FM.setFallbackHelper(field2);
|
|
103
|
-
t007.FM.setFieldListeners(field2);
|
|
103
|
+
t007.FM.toggleFilled(field2.querySelector(".t007-input")), t007.FM.setFallbackHelper(field2), t007.FM.setFieldListeners(field2);
|
|
104
104
|
field2.dataset.setUp = "true";
|
|
105
105
|
},
|
|
106
106
|
field({ isWrapper = false, label = "", type = "text", placeholder = "", custom = "", minSize, maxSize, minTotalSize, maxTotalSize, options = [], indeterminate = false, eyeToggler = true, passwordMeter = true, helperText = {}, className = "", fieldClassName = "", children, startIcon = "", endIcon = "", nativeIcon = "", passwordVisibleIcon = "", passwordHiddenIcon = "", ...otherProps }) {
|
|
@@ -134,19 +134,18 @@ var formManager = {
|
|
|
134
134
|
if (maxTotalSize) inputEl.setAttribute("maxtotalsize", maxTotalSize);
|
|
135
135
|
Object.keys(otherProps).forEach((key) => inputEl[key] = otherProps[key]);
|
|
136
136
|
labelEl.append(!isWrapper ? inputEl : children);
|
|
137
|
-
|
|
138
|
-
if (nativeTypes.includes(type) && nativeIcon) labelEl.append(createEl("i", { className: "t007-input-icon t007-input-native-icon", innerHTML: nativeIcon }));
|
|
137
|
+
if (nativeIconTypes.includes(type) && nativeIcon) labelEl.append(createEl("i", { className: "t007-input-icon t007-input-native-icon", innerHTML: nativeIcon }));
|
|
139
138
|
else if (endIcon) labelEl.append(createEl("i", { className: "t007-input-icon", innerHTML: endIcon }));
|
|
140
|
-
if (type === "password" && eyeToggler)
|
|
141
|
-
labelEl.append(
|
|
142
|
-
|
|
143
|
-
|
|
139
|
+
if (type === "password" && eyeToggler)
|
|
140
|
+
labelEl.append(
|
|
141
|
+
createEl("i", { role: "button", ariaLabel: "Show password", className: "t007-input-icon t007-input-password-visible-icon", innerHTML: passwordVisibleIcon || `<svg width="24" height="24"><path fill="rgba(0,0,0,.54)" d="M12 16q1.875 0 3.188-1.312Q16.5 13.375 16.5 11.5q0-1.875-1.312-3.188Q13.875 7 12 7q-1.875 0-3.188 1.312Q7.5 9.625 7.5 11.5q0 1.875 1.312 3.188Q10.125 16 12 16Zm0-1.8q-1.125 0-1.912-.788Q9.3 12.625 9.3 11.5t.788-1.913Q10.875 8.8 12 8.8t1.913.787q.787.788.787 1.913t-.787 1.912q-.788.788-1.913.788Zm0 4.8q-3.65 0-6.65-2.038-3-2.037-4.35-5.462 1.35-3.425 4.35-5.463Q8.35 4 12 4q3.65 0 6.65 2.037 3 2.038 4.35 5.463-1.35 3.425-4.35 5.462Q15.65 19 12 19Z"/></svg>` }),
|
|
142
|
+
createEl("i", { role: "button", ariaLabel: "Hide password", className: "t007-input-icon t007-input-password-hidden-icon", innerHTML: passwordHiddenIcon || `<svg width="24" height="24"><path fill="rgba(0,0,0,.54)" d="m19.8 22.6-4.2-4.15q-.875.275-1.762.413Q12.95 19 12 19q-3.775 0-6.725-2.087Q2.325 14.825 1 11.5q.525-1.325 1.325-2.463Q3.125 7.9 4.15 7L1.4 4.2l1.4-1.4 18.4 18.4ZM12 16q.275 0 .512-.025.238-.025.513-.1l-5.4-5.4q-.075.275-.1.513-.025.237-.025.512 0 1.875 1.312 3.188Q10.125 16 12 16Zm7.3.45-3.175-3.15q.175-.425.275-.862.1-.438.1-.938 0-1.875-1.312-3.188Q13.875 7 12 7q-.5 0-.938.1-.437.1-.862.3L7.65 4.85q1.025-.425 2.1-.638Q10.825 4 12 4q3.775 0 6.725 2.087Q21.675 8.175 23 11.5q-.575 1.475-1.512 2.738Q20.55 15.5 19.3 16.45Zm-4.625-4.6-3-3q.7-.125 1.288.112.587.238 1.012.688.425.45.613 1.038.187.587.087 1.162Z"/></svg>` })
|
|
143
|
+
);
|
|
144
144
|
if (helperText !== false) {
|
|
145
145
|
const helperLine = createEl("div", { className: "t007-input-helper-line" }), helperWrapper = createEl("div", { className: "t007-input-helper-text-wrapper", tabIndex: "-1" });
|
|
146
146
|
if (helperText.info) helperWrapper.append(createEl("p", { className: "t007-input-helper-text", textContent: helperText.info }, { violation: "none" }));
|
|
147
|
-
t007.FM
|
|
148
|
-
helperLine.append(helperWrapper);
|
|
149
|
-
field2.append(helperLine);
|
|
147
|
+
t007.FM.violationKeys?.forEach((key) => helperText[key] && helperWrapper.append(createEl("p", { className: "t007-input-helper-text", textContent: helperText[key] }, { violation: key })));
|
|
148
|
+
helperLine.append(helperWrapper), field2.append(helperLine);
|
|
150
149
|
}
|
|
151
150
|
if (passwordMeter && type === "password") {
|
|
152
151
|
const meter = createEl("div", { className: "t007-input-password-meter" }, { strengthLevel: "1" });
|
|
@@ -168,36 +167,26 @@ var formManager = {
|
|
|
168
167
|
form.validateOnClient = validateFormOnClient;
|
|
169
168
|
form.toggleGlobalError = toggleFormGlobalError;
|
|
170
169
|
const fields = form.getElementsByClassName("t007-input-field"), inputs = form.getElementsByClassName("t007-input");
|
|
171
|
-
Array.
|
|
172
|
-
form.addEventListener("input", ({ target }) =>
|
|
173
|
-
t007.FM.toggleFilled(target);
|
|
174
|
-
validateInput(target);
|
|
175
|
-
});
|
|
170
|
+
Array.prototype.forEach.call(fields, t007.FM.setUpField);
|
|
171
|
+
form.addEventListener("input", ({ target }) => (t007.FM.toggleFilled(target), validateInput(target)));
|
|
176
172
|
form.addEventListener("focusout", ({ target }) => validateInput(target, true));
|
|
177
173
|
form.addEventListener("submit", async (e) => {
|
|
178
|
-
|
|
174
|
+
form.classList.toggle("t007-input-submit-loading", true);
|
|
179
175
|
try {
|
|
180
176
|
e.preventDefault();
|
|
181
177
|
if (!validateFormOnClient()) return;
|
|
182
|
-
if (form.validateOnServer && !await form.validateOnServer()) {
|
|
183
|
-
|
|
184
|
-
form.addEventListener("input", () => toggleFormGlobalError(false), { once: true, useCapture: true });
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
form.onSubmit ? form.onSubmit() : form.submit();
|
|
178
|
+
if (form.validateOnServer && !await form.validateOnServer()) return toggleFormGlobalError(true), form.addEventListener("input", () => toggleFormGlobalError(false), { once: true, useCapture: true });
|
|
179
|
+
form.onSubmit ? form.onSubmit(e) : form.submit();
|
|
188
180
|
} catch (error) {
|
|
189
181
|
console.error(error);
|
|
190
182
|
}
|
|
191
|
-
|
|
183
|
+
form.classList.toggle("t007-input-submit-loading", false);
|
|
192
184
|
});
|
|
193
|
-
function toggleSubmitLoader(bool) {
|
|
194
|
-
form.classList.toggle("t007-input-submit-loading", bool);
|
|
195
|
-
}
|
|
196
185
|
function toggleError(input, bool, flag = false) {
|
|
197
186
|
const field2 = input.closest(".t007-input-field"), floatingLabel = field2.querySelector(".t007-input-floating-label");
|
|
198
187
|
if (bool && flag) {
|
|
199
188
|
input.setAttribute("data-error", "");
|
|
200
|
-
floatingLabel?.classList.add("t007-input-shake");
|
|
189
|
+
floatingLabel?.classList.add("t007-input-shake"), setTimeout(() => floatingLabel?.classList.remove("t007-input-shake"), 520);
|
|
201
190
|
} else if (!bool) input.removeAttribute("data-error");
|
|
202
191
|
toggleHelper(input, input.hasAttribute("data-error"));
|
|
203
192
|
}
|
|
@@ -205,28 +194,11 @@ var formManager = {
|
|
|
205
194
|
const field2 = input.closest(".t007-input-field"), violation = t007.FM.violationKeys.find((violation2) => input.Validity?.[violation2] || input.validity[violation2]) ?? "", helper = field2.querySelector(`.t007-input-helper-text[data-violation="${violation}"]`), fallbackHelper = field2.querySelector(`.t007-input-helper-text[data-violation="auto"]`);
|
|
206
195
|
input.closest(".t007-input-field").querySelectorAll(`.t007-input-helper-text:not([data-violation="${violation}"])`).forEach((helper2) => helper2?.classList.remove("t007-input-show"));
|
|
207
196
|
if (helper) helper.classList.toggle("t007-input-show", bool);
|
|
208
|
-
else if (fallbackHelper)
|
|
209
|
-
fallbackHelper.textContent = input.validationMessage;
|
|
210
|
-
fallbackHelper.classList.toggle("t007-input-show", bool);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
function forceRevalidate(input) {
|
|
214
|
-
input.checkValidity();
|
|
215
|
-
input.dispatchEvent(new Event("input"));
|
|
197
|
+
else if (fallbackHelper) fallbackHelper.textContent = input.validationMessage, fallbackHelper.classList.toggle("t007-input-show", bool);
|
|
216
198
|
}
|
|
217
199
|
function updatePasswordMeter(input) {
|
|
218
200
|
const passwordMeter = input.closest(".t007-input-field").querySelector(".t007-input-password-meter");
|
|
219
|
-
if (
|
|
220
|
-
const value = input.value?.trim();
|
|
221
|
-
let strengthLevel = 0;
|
|
222
|
-
if (value.length < Number(input.minLength ?? 0)) strengthLevel = 1;
|
|
223
|
-
else {
|
|
224
|
-
if (/[a-z]/.test(value)) strengthLevel++;
|
|
225
|
-
if (/[A-Z]/.test(value)) strengthLevel++;
|
|
226
|
-
if (/[0-9]/.test(value)) strengthLevel++;
|
|
227
|
-
if (/[\W_]/.test(value)) strengthLevel++;
|
|
228
|
-
}
|
|
229
|
-
passwordMeter.dataset.strengthLevel = strengthLevel;
|
|
201
|
+
if (passwordMeter) passwordMeter.dataset.strengthLevel = `${getStrengthLevel(input.value, Number(input.minLength ?? 0))}`;
|
|
230
202
|
}
|
|
231
203
|
function validateInput(input, flag = false) {
|
|
232
204
|
if (form.dataset.globalError || !input?.classList.contains("t007-input")) return;
|
|
@@ -236,7 +208,7 @@ var formManager = {
|
|
|
236
208
|
case "password":
|
|
237
209
|
value = input.value?.trim();
|
|
238
210
|
if (value === "") break;
|
|
239
|
-
const confirmPasswordInput = Array.
|
|
211
|
+
const confirmPasswordInput = Array.prototype.find.call(inputs, (input2) => (input2.custom ?? input2.getAttribute("custom")) === "confirm-password");
|
|
240
212
|
if (!confirmPasswordInput) break;
|
|
241
213
|
const confirmPasswordValue = confirmPasswordInput.value?.trim();
|
|
242
214
|
confirmPasswordInput.setCustomValidity(value !== confirmPasswordValue ? "Both passwords do not match" : "");
|
|
@@ -245,7 +217,7 @@ var formManager = {
|
|
|
245
217
|
case "confirm_password":
|
|
246
218
|
value = input.value?.trim();
|
|
247
219
|
if (value === "") break;
|
|
248
|
-
const passwordInput = Array.
|
|
220
|
+
const passwordInput = Array.prototype.find.call(inputs, (input2) => (input2.custom ?? input2.getAttribute("custom")) === "password");
|
|
249
221
|
if (!passwordInput) break;
|
|
250
222
|
const passwordValue = passwordInput.value?.trim();
|
|
251
223
|
errorBool = value !== passwordValue;
|
|
@@ -254,21 +226,17 @@ var formManager = {
|
|
|
254
226
|
case "onward_date":
|
|
255
227
|
if (input.min) break;
|
|
256
228
|
input.min = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
257
|
-
|
|
229
|
+
input.checkValidity(), input.dispatchEvent(new Event("input"));
|
|
230
|
+
break;
|
|
231
|
+
case "past_date":
|
|
232
|
+
if (input.max) break;
|
|
233
|
+
input.max = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
234
|
+
input.checkValidity(), input.dispatchEvent(new Event("input"));
|
|
258
235
|
break;
|
|
259
236
|
}
|
|
260
237
|
if (input.type === "file") {
|
|
261
238
|
input.Validity = {};
|
|
262
|
-
const { violation, message } = t007.FM.getFilesHelper(input.files ?? [], {
|
|
263
|
-
accept: input.accept,
|
|
264
|
-
multiple: input.multiple,
|
|
265
|
-
maxSize: input.maxSize ?? Number(input.getAttribute("maxsize")),
|
|
266
|
-
minSize: input.minSize ?? Number(input.getAttribute("minsize")),
|
|
267
|
-
maxTotalSize: input.maxTotalSize ?? Number(input.getAttribute("maxtotalsize")),
|
|
268
|
-
minTotalSize: input.minTotalSize ?? Number(input.getAttribute("mintotalsize")),
|
|
269
|
-
maxLength: input.maxLength ?? Number(input.getAttribute("maxlength")),
|
|
270
|
-
minLength: input.minLength ?? Number(input.getAttribute("minLength"))
|
|
271
|
-
});
|
|
239
|
+
const { violation, message } = t007.FM.getFilesHelper(input.files ?? [], { accept: input.accept, multiple: input.multiple, maxSize: input.maxSize ?? Number(input.getAttribute("maxsize")), minSize: input.minSize ?? Number(input.getAttribute("minsize")), maxTotalSize: input.maxTotalSize ?? Number(input.getAttribute("maxtotalsize")), minTotalSize: input.minTotalSize ?? Number(input.getAttribute("mintotalsize")), maxLength: input.maxLength ?? Number(input.getAttribute("maxlength")), minLength: input.minLength ?? Number(input.getAttribute("minLength")) });
|
|
272
240
|
errorBool = !!message;
|
|
273
241
|
input.setCustomValidity(message);
|
|
274
242
|
if (violation) input.Validity[violation] = true;
|
|
@@ -277,18 +245,22 @@ var formManager = {
|
|
|
277
245
|
toggleError(input, errorBool, flag);
|
|
278
246
|
if (errorBool) return;
|
|
279
247
|
if (input.type === "radio")
|
|
280
|
-
Array.
|
|
248
|
+
Array.prototype.forEach.call(
|
|
249
|
+
Array.prototype.filter.call(inputs, (i) => i.name == input.name),
|
|
250
|
+
(radio) => toggleError(radio, errorBool, flag)
|
|
251
|
+
);
|
|
281
252
|
}
|
|
282
253
|
function validateFormOnClient() {
|
|
283
|
-
Array.
|
|
254
|
+
Array.prototype.forEach.call(inputs, (input) => validateInput(input, true));
|
|
284
255
|
form.querySelector("input:invalid")?.focus();
|
|
285
|
-
return Array.
|
|
256
|
+
return Array.prototype.every.call(inputs, (input) => input.checkValidity());
|
|
286
257
|
}
|
|
287
258
|
function toggleFormGlobalError(bool) {
|
|
288
259
|
form.toggleAttribute("data-global-error", bool);
|
|
289
260
|
form.querySelectorAll(".t007-input-field").forEach((field2) => {
|
|
290
261
|
field2.querySelector(".t007-input")?.toggleAttribute("data-error", bool);
|
|
291
|
-
|
|
262
|
+
const floatingLabel = field2.querySelector(".t007-input-floating-label");
|
|
263
|
+
floatingLabel?.classList.toggle("t007-input-shake", bool), bool && setTimeout(() => floatingLabel?.classList.remove("t007-input-shake"), 520);
|
|
292
264
|
});
|
|
293
265
|
}
|
|
294
266
|
}
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
/** Browser date-like input types supported by input field helpers. */
|
|
4
|
+
declare const dateTypes: readonly ["date", "time", "datetime-local", "month"];
|
|
5
|
+
/** Union of all supported native date-like input types. */
|
|
6
|
+
type DateType = (typeof dateTypes)[number];
|
|
7
|
+
|
|
8
|
+
/** React change event alias used by input helpers. */
|
|
9
|
+
type CE<T> = React.ChangeEvent<T>;
|
|
10
|
+
/** React input event alias used by input helpers. */
|
|
11
|
+
type IE<T> = React.InputEvent<T>;
|
|
12
|
+
/** React change handler alias used by input helpers. */
|
|
13
|
+
type CEH<T> = React.ChangeEventHandler<T>;
|
|
14
|
+
/** React input handler alias used by input helpers. */
|
|
15
|
+
type IEH<T> = React.InputEventHandler<T>;
|
|
16
|
+
|
|
17
|
+
/** Base input element attributes used by non-select/non-textarea fields. */
|
|
18
|
+
type InputAttributes = React.InputHTMLAttributes<HTMLInputElement>;
|
|
19
|
+
/** Select element attributes used by select fields. */
|
|
20
|
+
type SelectAttributes = React.SelectHTMLAttributes<HTMLSelectElement>;
|
|
21
|
+
/** Textarea element attributes used by textarea fields. */
|
|
22
|
+
type TextareaAttributes = React.TextareaHTMLAttributes<HTMLTextAreaElement>;
|
|
23
|
+
|
|
24
|
+
/** Union type for all input elements */
|
|
25
|
+
type t007InputElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
|
26
|
+
|
|
27
|
+
/** Define all possible `ValidityState` keys mapped to string messages */
|
|
28
|
+
interface HelperTextMap extends Partial<Record<keyof ValidityState, string>> {
|
|
29
|
+
/** Informational helper text shown when there is no validation violation. */
|
|
30
|
+
info?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface BaseProps {
|
|
34
|
+
/** Wrap the field in its own container. */
|
|
35
|
+
isWrapper?: boolean;
|
|
36
|
+
/** Visible label text. */
|
|
37
|
+
label?: React.ReactNode;
|
|
38
|
+
/** Custom tokens for feature presets. */
|
|
39
|
+
custom?: "confirm_password" | "password" | "onward_date" | "past_date";
|
|
40
|
+
/** Class applied to the root field control. */
|
|
41
|
+
fieldClassName?: string;
|
|
42
|
+
/** Helper text shown under the field. */
|
|
43
|
+
helperText?: HelperTextMap | boolean;
|
|
44
|
+
/** External resolver error text. When provided, the field is forced into error visuals even without native validity violations. */
|
|
45
|
+
error?: string;
|
|
46
|
+
/** Start icon rendered inside the field control. */
|
|
47
|
+
// startIcon?: React.ReactNode;
|
|
48
|
+
/** End icon rendered inside the field control. */
|
|
49
|
+
endIcon?: React.ReactNode;
|
|
50
|
+
// onFlagError?: (args: { violation: keyof ValidityState | null; helper: string }) => void;
|
|
51
|
+
}
|
|
52
|
+
type PasswordInputProps = BaseProps & InputAttributes & PasswordInputAddon;
|
|
53
|
+
type FileInputProps = BaseProps & InputAttributes & FileInputAddon;
|
|
54
|
+
type CheckboxInputProps = BaseProps & InputAttributes & CheckboxInputAddon;
|
|
55
|
+
type DateInputProps = BaseProps & InputAttributes & DateInputAddon;
|
|
56
|
+
type GenericInputProps = BaseProps & InputAttributes & { type?: Exclude<React.HTMLInputTypeAttribute, "password" | "file" | "checkbox" | DateType> };
|
|
57
|
+
type TextareaElementProps = BaseProps & TextareaAttributes & { type: "textarea" };
|
|
58
|
+
type SelectElementProps = BaseProps & SelectAttributes & SelectElementAddon;
|
|
59
|
+
|
|
60
|
+
/** Union type for all input props, discriminated by the `type` property. */
|
|
61
|
+
type InputProps = PasswordInputProps | FileInputProps | CheckboxInputProps | DateInputProps | GenericInputProps | TextareaElementProps | SelectElementProps;
|
|
62
|
+
|
|
63
|
+
type WordsInputBaseProps = InputProps & {
|
|
64
|
+
/** Maximum count of words allowed. */
|
|
65
|
+
maxCount?: number;
|
|
66
|
+
/** Phrase template with %left%, %max%, %count% */
|
|
67
|
+
showCount?: string;
|
|
68
|
+
/** Whether to send the original change event instead of the plain string value. */
|
|
69
|
+
emitEventOnChange?: boolean;
|
|
70
|
+
};
|
|
71
|
+
type WordsInputStrictModeProps = WordsInputBaseProps & {
|
|
72
|
+
/** Whether to allow overflow of the word count. */
|
|
73
|
+
allowOverflow?: false;
|
|
74
|
+
};
|
|
75
|
+
type WordsInputLenientModeProps = WordsInputBaseProps & {
|
|
76
|
+
/** Whether to allow overflow of the word count. */
|
|
77
|
+
allowOverflow: true;
|
|
78
|
+
/** Helper text shown when the word count is exceeded. */
|
|
79
|
+
errorHelperText?: string;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Union type for all words input props. */
|
|
83
|
+
type WordsInputProps = WordsInputStrictModeProps | WordsInputLenientModeProps;
|
|
84
|
+
|
|
85
|
+
// Addon types for field options
|
|
86
|
+
|
|
87
|
+
interface PasswordInputAddon {
|
|
88
|
+
type: "password";
|
|
89
|
+
/** Show the password visibility toggler. */
|
|
90
|
+
eyeToggler?: boolean;
|
|
91
|
+
/** Enable the password strength meter. */
|
|
92
|
+
passwordMeter?: boolean;
|
|
93
|
+
passwordVisibleIcon?: React.ReactNode;
|
|
94
|
+
passwordHiddenIcon?: React.ReactNode;
|
|
95
|
+
}
|
|
96
|
+
interface FileInputAddon {
|
|
97
|
+
type: "file";
|
|
98
|
+
/** Maximum value length or count. */
|
|
99
|
+
maxSize?: number;
|
|
100
|
+
/** Minimum value length or count. */
|
|
101
|
+
minSize?: number;
|
|
102
|
+
/** Maximum total size allowed across the field value. */
|
|
103
|
+
maxTotalSize?: number;
|
|
104
|
+
/** Minimum total size allowed across the field value. */
|
|
105
|
+
minTotalSize?: number;
|
|
106
|
+
}
|
|
107
|
+
interface CheckboxInputAddon {
|
|
108
|
+
type: "checkbox";
|
|
109
|
+
/** Whether the checkbox is a multi-state checkbox. */
|
|
110
|
+
indeterminate?: boolean;
|
|
111
|
+
}
|
|
112
|
+
interface DateInputAddon {
|
|
113
|
+
type: DateType;
|
|
114
|
+
/** Native icon rendered by the browser control. */
|
|
115
|
+
nativeIcon?: React.ReactNode;
|
|
116
|
+
}
|
|
117
|
+
interface SelectElementAddon {
|
|
118
|
+
type: "select";
|
|
119
|
+
/** Options used by select-like fields. */
|
|
120
|
+
options?: string[] | readonly string[] | Array<{ value: string; option: string }>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
declare const Input: React.ForwardRefExoticComponent<InputProps & React.RefAttributes<t007InputElement>>;
|
|
124
|
+
|
|
125
|
+
declare const WordsInput: React.ForwardRefExoticComponent<WordsInputProps & React.RefAttributes<t007InputElement>>;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* FormManager hook to validate form-level inputs, does not force a form setup though.
|
|
129
|
+
* @param onSubmit Optional callback invoked after successful validation on submit.
|
|
130
|
+
* @param formRef Optional form ref; when omitted, the form passed to handlers is used.
|
|
131
|
+
* @returns handleSubmit: submit handler to attach to form elements, validate: function to trigger validation manually, fireInput: utility to trigger input events on fields.
|
|
132
|
+
*/
|
|
133
|
+
declare function useFormManager(onSubmit?: (e: React.FormEvent<HTMLFormElement>) => void, formRef?: React.RefObject<HTMLElement>): {
|
|
134
|
+
handleSubmit: (e: React.FormEvent<HTMLFormElement>) => void;
|
|
135
|
+
validate: (formEl?: HTMLElement) => boolean;
|
|
136
|
+
fireInput: (el?: t007InputElement | null) => boolean | undefined;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export { type BaseProps, type CE, type CEH, type HelperTextMap, type IE, type IEH, Input, type InputProps, WordsInput, type WordsInputBaseProps, type WordsInputLenientModeProps, type WordsInputProps, type WordsInputStrictModeProps, type t007InputElement, useFormManager };
|