@t007/input 0.0.3 → 0.0.4

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.
@@ -0,0 +1,439 @@
1
+ "use strict";
2
+ (() => {
3
+ // ../utils/dist/index.js
4
+ function isSameURL(src1, src2) {
5
+ if (typeof src1 !== "string" || typeof src2 !== "string" || !src1 || !src2) return false;
6
+ try {
7
+ const u1 = new URL(src1, window.location.href), u2 = new URL(src2, window.location.href);
8
+ return decodeURIComponent(u1.origin + u1.pathname) === decodeURIComponent(u2.origin + u2.pathname);
9
+ } catch {
10
+ return src1.replace(/\\/g, "/").split("?")[0].trim() === src2.replace(/\\/g, "/").split("?")[0].trim();
11
+ }
12
+ }
13
+ function createEl(tag, props, dataset, styles, el = tag ? document?.createElement(tag) : null) {
14
+ return assignEl(el, props, dataset, styles), el;
15
+ }
16
+ function assignEl(el, props, dataset, styles) {
17
+ if (!el) return;
18
+ if (props) {
19
+ for (const k of Object.keys(props)) if (props[k] !== void 0) el[k] = props[k];
20
+ }
21
+ if (dataset) {
22
+ for (const k of Object.keys(dataset)) if (dataset[k] !== void 0) el.dataset[k] = String(dataset[k]);
23
+ }
24
+ if (styles) {
25
+ for (const k of Object.keys(styles)) if (styles[k] !== void 0) el.style[k] = styles[k];
26
+ }
27
+ }
28
+ function loadResource(src, type = "style", { module, media, crossOrigin, integrity, referrerPolicy, nonce, fetchPriority, attempts = 3, retryKey = false } = {}, w = window) {
29
+ w.t007._resourceCache ??= {};
30
+ if (w.t007._resourceCache[src]) return w.t007._resourceCache[src];
31
+ const existing = type === "script" ? Array.prototype.find.call(w.document.scripts, (s) => isSameURL(s.src, src)) : type === "style" ? Array.prototype.find.call(w.document.styleSheets, (s) => isSameURL(s.href, src)) : null;
32
+ if (existing) return w.t007._resourceCache[src] = Promise.resolve(existing);
33
+ w.t007._resourceCache[src] = new Promise((resolve, reject) => {
34
+ (function tryLoad(remaining, el) {
35
+ const onerror = () => {
36
+ el?.remove?.();
37
+ if (remaining > 1) {
38
+ setTimeout(tryLoad, 1e3, remaining - 1);
39
+ console.warn(`Retrying ${type} load (${attempts - remaining + 1}): ${src}...`);
40
+ } else {
41
+ delete w.t007._resourceCache[src];
42
+ reject(new Error(`${type} load failed after ${attempts} attempts: ${src}`));
43
+ }
44
+ };
45
+ const url = retryKey && remaining < attempts ? `${src}${src.includes("?") ? "&" : "?"}_${retryKey}=${Date.now()}` : src;
46
+ if (type === "script") w.document.body.append(el = createEl("script", { src: url, type: module ? "module" : "text/javascript", crossOrigin, integrity, referrerPolicy, nonce, fetchPriority, onload: () => resolve(el), onerror }) || "");
47
+ else if (type === "style") w.document.head.append(el = createEl("link", { rel: "stylesheet", href: url, media, crossOrigin, integrity, referrerPolicy, nonce, fetchPriority, onload: () => resolve(el), onerror }) || "");
48
+ else reject(new Error(`Unsupported resource type: ${type}`));
49
+ })(attempts);
50
+ });
51
+ return w.t007._resourceCache[src];
52
+ }
53
+ function initScrollAssist(el, { pxPerSecond = 80, assistClassName = "tmg-video-controls-scroll-assist", vertical = true, horizontal = true } = {}) {
54
+ t007._scrollers ??= /* @__PURE__ */ new WeakMap();
55
+ t007._scroller_r_observer ??= new ResizeObserver((entries) => entries.forEach(({ target }) => t007._scrollers.get(target)?.update()));
56
+ t007._scroller_m_observer ??= new MutationObserver((entries) => {
57
+ const els = /* @__PURE__ */ new Set();
58
+ for (const entry of entries) {
59
+ let node = entry.target instanceof Element ? entry.target : null;
60
+ while (node && !t007._scrollers.has(node)) node = node.parentElement;
61
+ if (node) els.add(node);
62
+ }
63
+ for (const el2 of els) t007._scrollers.get(el2)?.update();
64
+ });
65
+ const parent = el?.parentElement;
66
+ if (!parent || t007._scrollers.has(el)) return;
67
+ const assist = {};
68
+ let scrollId = null, last = performance.now(), assistWidth = 20, assistHeight = 20;
69
+ const update = () => {
70
+ const hasInteractive = !!parent.querySelector('button, a[href], input, select, textarea, [contenteditable="true"], [tabindex]:not([tabindex="-1"])');
71
+ if (horizontal) {
72
+ const w = assist.left?.offsetWidth || assistWidth, check = hasInteractive ? el.clientWidth < w * 2 : false;
73
+ assist.left.style.display = check ? "none" : el.scrollLeft > 0 ? "block" : "none";
74
+ assist.right.style.display = check ? "none" : el.scrollLeft + el.clientWidth < el.scrollWidth - 1 ? "block" : "none";
75
+ assistWidth = w;
76
+ }
77
+ if (vertical) {
78
+ const h = assist.up?.offsetHeight || assistHeight, check = hasInteractive ? el.clientHeight < h * 2 : false;
79
+ assist.up.style.display = check ? "none" : el.scrollTop > 0 ? "block" : "none";
80
+ assist.down.style.display = check ? "none" : el.scrollTop + el.clientHeight < el.scrollHeight - 1 ? "block" : "none";
81
+ assistHeight = h;
82
+ }
83
+ };
84
+ const scroll = (dir) => {
85
+ const frame = () => {
86
+ const now = performance.now(), dt = now - last;
87
+ last = now;
88
+ const d = pxPerSecond * dt / 1e3;
89
+ if (dir === "left") el.scrollLeft = Math.max(0, el.scrollLeft - d);
90
+ if (dir === "right") el.scrollLeft = Math.min(el.scrollWidth - el.clientWidth, el.scrollLeft + d);
91
+ if (dir === "up") el.scrollTop = Math.max(0, el.scrollTop - d);
92
+ if (dir === "down") el.scrollTop = Math.min(el.scrollHeight - el.clientHeight, el.scrollTop + d);
93
+ scrollId = requestAnimationFrame(frame);
94
+ };
95
+ last = performance.now();
96
+ frame();
97
+ };
98
+ const stop = () => (cancelAnimationFrame(scrollId ?? 0), scrollId = null);
99
+ const addAssist = (dir) => {
100
+ const div = createEl("div", { className: assistClassName }, { scrollDirection: dir }, { display: "none" });
101
+ if (!div) return;
102
+ ["pointerenter", "dragenter"].forEach((evt) => div.addEventListener(evt, () => scroll(dir)));
103
+ ["pointerleave", "pointerup", "pointercancel", "dragleave", "dragend"].forEach((evt) => div.addEventListener(evt, stop));
104
+ dir === "left" || dir === "up" ? parent.insertBefore(div, el) : parent.append(div);
105
+ assist[dir] = div;
106
+ };
107
+ if (horizontal) ["left", "right"].forEach(addAssist);
108
+ if (vertical) ["up", "down"].forEach(addAssist);
109
+ el.addEventListener("scroll", update);
110
+ t007._scroller_r_observer.observe(el);
111
+ t007._scroller_m_observer.observe(el, { childList: true, subtree: true, characterData: true });
112
+ t007._scrollers.set(el, {
113
+ update,
114
+ destroy() {
115
+ stop();
116
+ el.removeEventListener("scroll", update);
117
+ t007._scroller_r_observer.unobserve(el);
118
+ t007._scrollers.delete(el);
119
+ Object.values(assist).forEach((a) => a.remove());
120
+ }
121
+ });
122
+ return update(), t007._scrollers.get(el);
123
+ }
124
+ if (typeof window !== "undefined") {
125
+ window.t007 ??= {};
126
+ window.T007_TOAST_JS_SRC ??= `https://cdn.jsdelivr.net/npm/@t007/toast@latest`;
127
+ window.T007_INPUT_JS_SRC ??= `https://cdn.jsdelivr.net/npm/@t007/input@latest`;
128
+ window.T007_DIALOG_JS_SRC ??= `https://cdn.jsdelivr.net/npm/@t007/dialog@latest`;
129
+ window.T007_TOAST_CSS_SRC ??= `https://cdn.jsdelivr.net/npm/@t007/toast@latest/style.css`;
130
+ window.T007_INPUT_CSS_SRC ??= `https://cdn.jsdelivr.net/npm/@t007/input@latest/style.css`;
131
+ window.T007_DIALOG_CSS_SRC ??= `https://cdn.jsdelivr.net/npm/@t007/dialog@latest/style.css`;
132
+ }
133
+
134
+ // src/index.js
135
+ var formManager = {
136
+ forms: document.getElementsByClassName("t007-input-form"),
137
+ violationKeys: ["valueMissing", "typeMismatch", "patternMismatch", "stepMismatch", "tooShort", "tooLong", "rangeUnderflow", "rangeOverflow", "badInput", "customError"],
138
+ init() {
139
+ t007.FM.observeDOMForFields();
140
+ Array.from(t007.FM.forms).forEach(t007.FM.handleFormValidation);
141
+ },
142
+ observeDOMForFields() {
143
+ new MutationObserver((mutations) => {
144
+ for (const mutation of mutations) {
145
+ for (const node of mutation.addedNodes) {
146
+ if (!node.tagName || !(node?.classList?.contains("t007-input-field") || node?.querySelector?.(".t007-input-field"))) continue;
147
+ for (const field2 of [...node.querySelector(".t007-input-field") ? node.querySelectorAll(".t007-input-field") : [node]]) t007.FM.setUpField(field2);
148
+ }
149
+ }
150
+ }).observe(document.body, { childList: true, subtree: true });
151
+ },
152
+ getFilesHelper(files, opts) {
153
+ if (!files || !files.length) return { violation: null, message: "" };
154
+ const totalFiles = files.length;
155
+ let totalSize = 0;
156
+ let currFiles = 0;
157
+ 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)}` });
158
+ 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)}` });
159
+ for (const file of files) {
160
+ currFiles++;
161
+ totalSize += file.size;
162
+ if (opts.accept) {
163
+ const acceptedTypes = opts.accept.split(",").map((type) => type.trim().replace(/^[*\.]+|[*\.]+$/g, "")).filter(Boolean) || [];
164
+ if (!acceptedTypes.some((type) => file.type.includes(type))) return { violation: "typeMismatch", message: `File${currFiles > 1 ? currFiles : ""} type of '${file.type}' is not accepted.` };
165
+ }
166
+ if (opts.maxSize && file.size > opts.maxSize) return setMaxError(file.size, opts.maxSize, currFiles);
167
+ if (opts.minSize && file.size < opts.minSize) return setMinError(file.size, opts.minSize, currFiles);
168
+ if (opts.multiple) {
169
+ if (opts.maxTotalSize && totalSize > opts.maxTotalSize) return setMaxError(totalSize, opts.maxTotalSize);
170
+ if (opts.minTotalSize && totalSize < opts.minTotalSize) return setMinError(totalSize, opts.minTotalSize);
171
+ 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"}` };
172
+ 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"}` };
173
+ }
174
+ }
175
+ return { violation: null, message: "" };
176
+ },
177
+ formatSize(size, decimals = 3, base = 1e3) {
178
+ if (size < base) return `${size} byte${size == 1 ? "" : "s"}`;
179
+ 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);
180
+ return `${(size / Math.pow(base, exponent)).toFixed(decimals).replace(/\.0+$/, "")} ${units[exponent]}`;
181
+ },
182
+ togglePasswordType: (input) => input.type = input.type === "password" ? "text" : "password",
183
+ toggleFilled: (input) => input?.toggleAttribute("data-filled", input.type === "checkbox" || input.type === "radio" ? input.checked : input.value !== "" || input.files?.length > 0),
184
+ setFallbackHelper(field2) {
185
+ const helperTextWrapper = field2?.querySelector(".t007-input-helper-text-wrapper");
186
+ if (!helperTextWrapper || helperTextWrapper.querySelector(".t007-input-helper-text[data-violation='auto']")) return;
187
+ helperTextWrapper.append(createEl("p", { className: "t007-input-helper-text" }, { violation: "auto" }));
188
+ },
189
+ setFieldListeners(field2) {
190
+ if (!field2) return;
191
+ const input = field2.querySelector(".t007-input"), floatingLabel = field2.querySelector(".t007-input-floating-label"), eyeOpen = field2.querySelector(".t007-input-password-visible-icon"), eyeClosed = field2.querySelector(".t007-input-password-hidden-icon");
192
+ if (input.type === "file")
193
+ input.addEventListener("input", async () => {
194
+ const file = input.files?.[0], img = new Image();
195
+ img.onload = () => {
196
+ input.style.setProperty("--t007-input-image-src", `url(${src})`);
197
+ input.classList.add("t007-input-image-selected");
198
+ setTimeout(() => URL.revokeObjectURL(src), 1e3);
199
+ };
200
+ img.onerror = () => {
201
+ input.style.removeProperty("--t007-input-image-src");
202
+ input.classList.remove("t007-input-image-selected");
203
+ URL.revokeObjectURL(src);
204
+ };
205
+ let src;
206
+ if (file?.type?.startsWith("image")) src = URL.createObjectURL(file);
207
+ else if (file?.type?.startsWith("video")) {
208
+ src = await new Promise((resolve) => {
209
+ let video = createEl("video"), canvas = createEl("canvas"), context = canvas.getContext("2d");
210
+ video.ontimeupdate = () => {
211
+ context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
212
+ canvas.toBlob((blob) => resolve(URL.createObjectURL(blob)));
213
+ URL.revokeObjectURL(video.src);
214
+ video = video.src = video.onloadedmetadata = video.ontimeupdate = null;
215
+ };
216
+ video.onloadeddata = () => video.currentTime = 3;
217
+ video.src = URL.createObjectURL(file);
218
+ });
219
+ }
220
+ if (!src) {
221
+ input.style.removeProperty("--t007-input-image-src");
222
+ input.classList.remove("t007-input-image-selected");
223
+ return;
224
+ }
225
+ img.src = src;
226
+ });
227
+ if (floatingLabel) floatingLabel.ontransitionend = () => floatingLabel.classList.remove("t007-input-shake");
228
+ if (eyeOpen && eyeClosed) eyeOpen.onclick = eyeClosed.onclick = () => t007.FM.togglePasswordType(input);
229
+ initScrollAssist(field2.querySelector(".t007-input-helper-text-wrapper"), { vertical: false });
230
+ },
231
+ setUpField(field2) {
232
+ if (field2.dataset.setUp) return;
233
+ t007.FM.toggleFilled(field2.querySelector(".t007-input"));
234
+ t007.FM.setFallbackHelper(field2);
235
+ t007.FM.setFieldListeners(field2);
236
+ field2.dataset.setUp = "true";
237
+ },
238
+ 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 }) {
239
+ const isSelect = type === "select", isTextArea = type === "textarea", isCheckboxOrRadio = type === "checkbox" || type === "radio", field2 = createEl("div", { className: `t007-input-field${isWrapper ? " t007-input-is-wrapper" : ""}${indeterminate ? " t007-input-indeterminate" : ""}${!!nativeIcon ? " t007-input-icon-override" : ""}${helperText === false ? " t007-input-no-helper" : ""}${fieldClassName ? ` ${fieldClassName}` : ""}` }), labelEl = createEl("label", { className: isCheckboxOrRadio ? `t007-input-${type}-wrapper` : "t007-input-wrapper" });
240
+ field2.append(labelEl);
241
+ if (isCheckboxOrRadio) {
242
+ labelEl.innerHTML = `
243
+ <span class="t007-input-${type}-box">
244
+ <span class="t007-input-${type}-tag"></span>
245
+ </span>
246
+ <span class="t007-input-${type}-label">${label}</span>
247
+ `;
248
+ } else {
249
+ const outline = createEl("span", { className: "t007-input-outline" });
250
+ outline.innerHTML = `
251
+ <span class="t007-input-outline-leading"></span>
252
+ <span class="t007-input-outline-notch">
253
+ <span class="t007-input-floating-label">${label}</span>
254
+ </span>
255
+ <span class="t007-input-outline-trailing"></span>
256
+ `;
257
+ labelEl.append(outline);
258
+ }
259
+ const inputEl = field2.inputEl = createEl(isTextArea ? "textarea" : isSelect ? "select" : "input", { className: `t007-input${className ? ` ${className}` : ""}`, placeholder });
260
+ if (isSelect && Array.isArray(options)) inputEl.innerHTML = options.map((opt) => typeof opt === "string" ? `<option value="${opt}">${opt}</option>` : `<option value="${opt.value}">${opt.option}</option>`).join("");
261
+ if (!isSelect && !isTextArea) inputEl.type = type;
262
+ if (custom) inputEl.setAttribute("custom", custom);
263
+ if (minSize) inputEl.setAttribute("minsize", minSize);
264
+ if (maxSize) inputEl.setAttribute("maxsize", maxSize);
265
+ if (minTotalSize) inputEl.setAttribute("mintotalsize", minTotalSize);
266
+ if (maxTotalSize) inputEl.setAttribute("maxtotalsize", maxTotalSize);
267
+ Object.keys(otherProps).forEach((key) => inputEl[key] = otherProps[key]);
268
+ labelEl.append(!isWrapper ? inputEl : children);
269
+ const nativeTypes = ["date", "time", "month", "datetime-local"];
270
+ if (nativeTypes.includes(type) && nativeIcon) labelEl.append(createEl("i", { className: "t007-input-icon t007-input-native-icon", innerHTML: nativeIcon }));
271
+ else if (endIcon) labelEl.append(createEl("i", { className: "t007-input-icon", innerHTML: endIcon }));
272
+ if (type === "password" && eyeToggler) {
273
+ labelEl.append(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>` }));
274
+ labelEl.append(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>` }));
275
+ }
276
+ if (helperText !== false) {
277
+ const helperLine = createEl("div", { className: "t007-input-helper-line" }), helperWrapper = createEl("div", { className: "t007-input-helper-text-wrapper", tabIndex: "-1" });
278
+ if (helperText.info) helperWrapper.append(createEl("p", { className: "t007-input-helper-text", textContent: helperText.info }, { violation: "none" }));
279
+ t007.FM?.violationKeys?.forEach((key) => helperText[key] && helperWrapper.append(createEl("p", { className: "t007-input-helper-text", textContent: helperText[key] }, { violation: key })));
280
+ helperLine.append(helperWrapper);
281
+ field2.append(helperLine);
282
+ }
283
+ if (passwordMeter && type === "password") {
284
+ const meter = createEl("div", { className: "t007-input-password-meter" }, { strengthLevel: "1" });
285
+ meter.innerHTML = `
286
+ <div class="t007-input-password-strength-meter">
287
+ <div class="t007-input-p-weak"></div>
288
+ <div class="t007-input-p-fair"></div>
289
+ <div class="t007-input-p-strong"></div>
290
+ <div class="t007-input-p-very-strong"></div>
291
+ </div>
292
+ `;
293
+ field2.append(meter);
294
+ }
295
+ return field2;
296
+ },
297
+ handleFormValidation(form) {
298
+ if (!form?.classList.contains("t007-input-form") || form.dataset?.isValidating) return;
299
+ form.dataset.isValidating = "true";
300
+ form.validateOnClient = validateFormOnClient;
301
+ form.toggleGlobalError = toggleFormGlobalError;
302
+ const fields = form.getElementsByClassName("t007-input-field"), inputs = form.getElementsByClassName("t007-input");
303
+ Array.from(fields).forEach(t007.FM.setUpField);
304
+ form.addEventListener("input", ({ target }) => {
305
+ t007.FM.toggleFilled(target);
306
+ validateInput(target);
307
+ });
308
+ form.addEventListener("focusout", ({ target }) => validateInput(target, true));
309
+ form.addEventListener("submit", async (e) => {
310
+ toggleSubmitLoader(true);
311
+ try {
312
+ e.preventDefault();
313
+ if (!validateFormOnClient()) return;
314
+ if (form.validateOnServer && !await form.validateOnServer()) {
315
+ toggleFormGlobalError(true);
316
+ form.addEventListener("input", () => toggleFormGlobalError(false), { once: true, useCapture: true });
317
+ return;
318
+ }
319
+ form.onSubmit ? form.onSubmit() : form.submit();
320
+ } catch (error) {
321
+ console.error(error);
322
+ }
323
+ toggleSubmitLoader(false);
324
+ });
325
+ function toggleSubmitLoader(bool) {
326
+ form.classList.toggle("t007-input-submit-loading", bool);
327
+ }
328
+ function toggleError(input, bool, flag = false) {
329
+ const field2 = input.closest(".t007-input-field"), floatingLabel = field2.querySelector(".t007-input-floating-label");
330
+ if (bool && flag) {
331
+ input.setAttribute("data-error", "");
332
+ floatingLabel?.classList.add("t007-input-shake");
333
+ } else if (!bool) input.removeAttribute("data-error");
334
+ toggleHelper(input, input.hasAttribute("data-error"));
335
+ }
336
+ function toggleHelper(input, bool) {
337
+ 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"]`);
338
+ input.closest(".t007-input-field").querySelectorAll(`.t007-input-helper-text:not([data-violation="${violation}"])`).forEach((helper2) => helper2?.classList.remove("t007-input-show"));
339
+ if (helper) helper.classList.toggle("t007-input-show", bool);
340
+ else if (fallbackHelper) {
341
+ fallbackHelper.textContent = input.validationMessage;
342
+ fallbackHelper.classList.toggle("t007-input-show", bool);
343
+ }
344
+ }
345
+ function forceRevalidate(input) {
346
+ input.checkValidity();
347
+ input.dispatchEvent(new Event("input"));
348
+ }
349
+ function updatePasswordMeter(input) {
350
+ const passwordMeter = input.closest(".t007-input-field").querySelector(".t007-input-password-meter");
351
+ if (!passwordMeter) return;
352
+ const value = input.value?.trim();
353
+ let strengthLevel = 0;
354
+ if (value.length < Number(input.minLength ?? 0)) strengthLevel = 1;
355
+ else {
356
+ if (/[a-z]/.test(value)) strengthLevel++;
357
+ if (/[A-Z]/.test(value)) strengthLevel++;
358
+ if (/[0-9]/.test(value)) strengthLevel++;
359
+ if (/[\W_]/.test(value)) strengthLevel++;
360
+ }
361
+ passwordMeter.dataset.strengthLevel = strengthLevel;
362
+ }
363
+ function validateInput(input, flag = false) {
364
+ if (form.dataset.globalError || !input?.classList.contains("t007-input")) return;
365
+ updatePasswordMeter(input);
366
+ let value, errorBool;
367
+ switch (input.custom ?? input.getAttribute("custom")) {
368
+ case "password":
369
+ value = input.value?.trim();
370
+ if (value === "") break;
371
+ const confirmPasswordInput = Array.from(inputs).find((input2) => (input2.custom ?? input2.getAttribute("custom")) === "confirm-password");
372
+ if (!confirmPasswordInput) break;
373
+ const confirmPasswordValue = confirmPasswordInput.value?.trim();
374
+ confirmPasswordInput.setCustomValidity(value !== confirmPasswordValue ? "Both passwords do not match" : "");
375
+ toggleError(confirmPasswordInput, value !== confirmPasswordValue, flag);
376
+ break;
377
+ case "confirm_password":
378
+ value = input.value?.trim();
379
+ if (value === "") break;
380
+ const passwordInput = Array.from(inputs).find((input2) => (input2.custom ?? input2.getAttribute("custom")) === "password");
381
+ if (!passwordInput) break;
382
+ const passwordValue = passwordInput.value?.trim();
383
+ errorBool = value !== passwordValue;
384
+ input.setCustomValidity(errorBool ? "Both passwords do not match" : "");
385
+ break;
386
+ case "onward_date":
387
+ if (input.min) break;
388
+ input.min = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
389
+ forceRevalidate(input);
390
+ break;
391
+ }
392
+ if (input.type === "file") {
393
+ input.Validity = {};
394
+ const { violation, message } = t007.FM.getFilesHelper(input.files ?? [], {
395
+ accept: input.accept,
396
+ multiple: input.multiple,
397
+ maxSize: input.maxSize ?? Number(input.getAttribute("maxsize")),
398
+ minSize: input.minSize ?? Number(input.getAttribute("minsize")),
399
+ maxTotalSize: input.maxTotalSize ?? Number(input.getAttribute("maxtotalsize")),
400
+ minTotalSize: input.minTotalSize ?? Number(input.getAttribute("mintotalsize")),
401
+ maxLength: input.maxLength ?? Number(input.getAttribute("maxlength")),
402
+ minLength: input.minLength ?? Number(input.getAttribute("minLength"))
403
+ });
404
+ errorBool = !!message;
405
+ input.setCustomValidity(message);
406
+ if (violation) input.Validity[violation] = true;
407
+ }
408
+ errorBool = errorBool ?? !input.validity?.valid;
409
+ toggleError(input, errorBool, flag);
410
+ if (errorBool) return;
411
+ if (input.type === "radio")
412
+ Array.from(inputs)?.filter((i) => i.name == input.name)?.forEach((radio) => toggleError(radio, errorBool, flag));
413
+ }
414
+ function validateFormOnClient() {
415
+ Array.from(inputs).forEach((input) => validateInput(input, true));
416
+ form.querySelector("input:invalid")?.focus();
417
+ return Array.from(inputs).every((input) => input.checkValidity());
418
+ }
419
+ function toggleFormGlobalError(bool) {
420
+ form.toggleAttribute("data-global-error", bool);
421
+ form.querySelectorAll(".t007-input-field").forEach((field2) => {
422
+ field2.querySelector(".t007-input")?.toggleAttribute("data-error", bool);
423
+ if (bool) field2.querySelector(".t007-input-floating-label")?.classList.add("t007-input-shake");
424
+ });
425
+ }
426
+ }
427
+ };
428
+ var { field, handleFormValidation } = formManager;
429
+ if (typeof window !== "undefined") {
430
+ t007.FM = formManager;
431
+ t007.field = field;
432
+ t007.handleFormValidation = handleFormValidation;
433
+ window.field ??= t007.field;
434
+ window.handleFormValidation ??= t007.handleFormValidation;
435
+ console.log("%cT007 Input helpers attached to window!", "color: darkturquoise");
436
+ loadResource(T007_INPUT_CSS_SRC);
437
+ t007.FM.init();
438
+ }
439
+ })();
package/dist/index.js ADDED
@@ -0,0 +1,311 @@
1
+ // src/index.js
2
+ import { createEl, loadResource, initScrollAssist } from "@t007/utils";
3
+ var formManager = {
4
+ forms: document.getElementsByClassName("t007-input-form"),
5
+ violationKeys: ["valueMissing", "typeMismatch", "patternMismatch", "stepMismatch", "tooShort", "tooLong", "rangeUnderflow", "rangeOverflow", "badInput", "customError"],
6
+ init() {
7
+ t007.FM.observeDOMForFields();
8
+ Array.from(t007.FM.forms).forEach(t007.FM.handleFormValidation);
9
+ },
10
+ observeDOMForFields() {
11
+ new MutationObserver((mutations) => {
12
+ for (const mutation of mutations) {
13
+ for (const node of mutation.addedNodes) {
14
+ if (!node.tagName || !(node?.classList?.contains("t007-input-field") || node?.querySelector?.(".t007-input-field"))) continue;
15
+ for (const field2 of [...node.querySelector(".t007-input-field") ? node.querySelectorAll(".t007-input-field") : [node]]) t007.FM.setUpField(field2);
16
+ }
17
+ }
18
+ }).observe(document.body, { childList: true, subtree: true });
19
+ },
20
+ getFilesHelper(files, opts) {
21
+ if (!files || !files.length) return { violation: null, message: "" };
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
+ },
50
+ togglePasswordType: (input) => input.type = input.type === "password" ? "text" : "password",
51
+ toggleFilled: (input) => input?.toggleAttribute("data-filled", input.type === "checkbox" || input.type === "radio" ? input.checked : input.value !== "" || input.files?.length > 0),
52
+ setFallbackHelper(field2) {
53
+ const helperTextWrapper = field2?.querySelector(".t007-input-helper-text-wrapper");
54
+ if (!helperTextWrapper || helperTextWrapper.querySelector(".t007-input-helper-text[data-violation='auto']")) return;
55
+ helperTextWrapper.append(createEl("p", { className: "t007-input-helper-text" }, { violation: "auto" }));
56
+ },
57
+ setFieldListeners(field2) {
58
+ if (!field2) return;
59
+ const input = field2.querySelector(".t007-input"), floatingLabel = field2.querySelector(".t007-input-floating-label"), eyeOpen = field2.querySelector(".t007-input-password-visible-icon"), eyeClosed = field2.querySelector(".t007-input-password-hidden-icon");
60
+ if (input.type === "file")
61
+ input.addEventListener("input", async () => {
62
+ const file = input.files?.[0], img = new Image();
63
+ img.onload = () => {
64
+ input.style.setProperty("--t007-input-image-src", `url(${src})`);
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
+ };
73
+ let src;
74
+ if (file?.type?.startsWith("image")) src = URL.createObjectURL(file);
75
+ else if (file?.type?.startsWith("video")) {
76
+ src = await new Promise((resolve) => {
77
+ let video = createEl("video"), canvas = createEl("canvas"), context = canvas.getContext("2d");
78
+ video.ontimeupdate = () => {
79
+ context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
80
+ canvas.toBlob((blob) => resolve(URL.createObjectURL(blob)));
81
+ URL.revokeObjectURL(video.src);
82
+ video = video.src = video.onloadedmetadata = video.ontimeupdate = null;
83
+ };
84
+ video.onloadeddata = () => video.currentTime = 3;
85
+ video.src = URL.createObjectURL(file);
86
+ });
87
+ }
88
+ if (!src) {
89
+ input.style.removeProperty("--t007-input-image-src");
90
+ input.classList.remove("t007-input-image-selected");
91
+ return;
92
+ }
93
+ img.src = src;
94
+ });
95
+ if (floatingLabel) floatingLabel.ontransitionend = () => floatingLabel.classList.remove("t007-input-shake");
96
+ if (eyeOpen && eyeClosed) eyeOpen.onclick = eyeClosed.onclick = () => t007.FM.togglePasswordType(input);
97
+ initScrollAssist(field2.querySelector(".t007-input-helper-text-wrapper"), { vertical: false });
98
+ },
99
+ setUpField(field2) {
100
+ if (field2.dataset.setUp) return;
101
+ t007.FM.toggleFilled(field2.querySelector(".t007-input"));
102
+ t007.FM.setFallbackHelper(field2);
103
+ t007.FM.setFieldListeners(field2);
104
+ field2.dataset.setUp = "true";
105
+ },
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 }) {
107
+ const isSelect = type === "select", isTextArea = type === "textarea", isCheckboxOrRadio = type === "checkbox" || type === "radio", field2 = createEl("div", { className: `t007-input-field${isWrapper ? " t007-input-is-wrapper" : ""}${indeterminate ? " t007-input-indeterminate" : ""}${!!nativeIcon ? " t007-input-icon-override" : ""}${helperText === false ? " t007-input-no-helper" : ""}${fieldClassName ? ` ${fieldClassName}` : ""}` }), labelEl = createEl("label", { className: isCheckboxOrRadio ? `t007-input-${type}-wrapper` : "t007-input-wrapper" });
108
+ field2.append(labelEl);
109
+ if (isCheckboxOrRadio) {
110
+ labelEl.innerHTML = `
111
+ <span class="t007-input-${type}-box">
112
+ <span class="t007-input-${type}-tag"></span>
113
+ </span>
114
+ <span class="t007-input-${type}-label">${label}</span>
115
+ `;
116
+ } else {
117
+ const outline = createEl("span", { className: "t007-input-outline" });
118
+ outline.innerHTML = `
119
+ <span class="t007-input-outline-leading"></span>
120
+ <span class="t007-input-outline-notch">
121
+ <span class="t007-input-floating-label">${label}</span>
122
+ </span>
123
+ <span class="t007-input-outline-trailing"></span>
124
+ `;
125
+ labelEl.append(outline);
126
+ }
127
+ const inputEl = field2.inputEl = createEl(isTextArea ? "textarea" : isSelect ? "select" : "input", { className: `t007-input${className ? ` ${className}` : ""}`, placeholder });
128
+ if (isSelect && Array.isArray(options)) inputEl.innerHTML = options.map((opt) => typeof opt === "string" ? `<option value="${opt}">${opt}</option>` : `<option value="${opt.value}">${opt.option}</option>`).join("");
129
+ if (!isSelect && !isTextArea) inputEl.type = type;
130
+ if (custom) inputEl.setAttribute("custom", custom);
131
+ if (minSize) inputEl.setAttribute("minsize", minSize);
132
+ if (maxSize) inputEl.setAttribute("maxsize", maxSize);
133
+ if (minTotalSize) inputEl.setAttribute("mintotalsize", minTotalSize);
134
+ if (maxTotalSize) inputEl.setAttribute("maxtotalsize", maxTotalSize);
135
+ Object.keys(otherProps).forEach((key) => inputEl[key] = otherProps[key]);
136
+ labelEl.append(!isWrapper ? inputEl : children);
137
+ const nativeTypes = ["date", "time", "month", "datetime-local"];
138
+ if (nativeTypes.includes(type) && nativeIcon) labelEl.append(createEl("i", { className: "t007-input-icon t007-input-native-icon", innerHTML: nativeIcon }));
139
+ else if (endIcon) labelEl.append(createEl("i", { className: "t007-input-icon", innerHTML: endIcon }));
140
+ if (type === "password" && eyeToggler) {
141
+ labelEl.append(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
+ labelEl.append(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
+ if (helperText !== false) {
145
+ const helperLine = createEl("div", { className: "t007-input-helper-line" }), helperWrapper = createEl("div", { className: "t007-input-helper-text-wrapper", tabIndex: "-1" });
146
+ if (helperText.info) helperWrapper.append(createEl("p", { className: "t007-input-helper-text", textContent: helperText.info }, { violation: "none" }));
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);
149
+ field2.append(helperLine);
150
+ }
151
+ if (passwordMeter && type === "password") {
152
+ const meter = createEl("div", { className: "t007-input-password-meter" }, { strengthLevel: "1" });
153
+ meter.innerHTML = `
154
+ <div class="t007-input-password-strength-meter">
155
+ <div class="t007-input-p-weak"></div>
156
+ <div class="t007-input-p-fair"></div>
157
+ <div class="t007-input-p-strong"></div>
158
+ <div class="t007-input-p-very-strong"></div>
159
+ </div>
160
+ `;
161
+ field2.append(meter);
162
+ }
163
+ return field2;
164
+ },
165
+ handleFormValidation(form) {
166
+ if (!form?.classList.contains("t007-input-form") || form.dataset?.isValidating) return;
167
+ form.dataset.isValidating = "true";
168
+ form.validateOnClient = validateFormOnClient;
169
+ form.toggleGlobalError = toggleFormGlobalError;
170
+ const fields = form.getElementsByClassName("t007-input-field"), inputs = form.getElementsByClassName("t007-input");
171
+ Array.from(fields).forEach(t007.FM.setUpField);
172
+ form.addEventListener("input", ({ target }) => {
173
+ t007.FM.toggleFilled(target);
174
+ validateInput(target);
175
+ });
176
+ form.addEventListener("focusout", ({ target }) => validateInput(target, true));
177
+ form.addEventListener("submit", async (e) => {
178
+ toggleSubmitLoader(true);
179
+ try {
180
+ e.preventDefault();
181
+ if (!validateFormOnClient()) return;
182
+ if (form.validateOnServer && !await form.validateOnServer()) {
183
+ toggleFormGlobalError(true);
184
+ form.addEventListener("input", () => toggleFormGlobalError(false), { once: true, useCapture: true });
185
+ return;
186
+ }
187
+ form.onSubmit ? form.onSubmit() : form.submit();
188
+ } catch (error) {
189
+ console.error(error);
190
+ }
191
+ toggleSubmitLoader(false);
192
+ });
193
+ function toggleSubmitLoader(bool) {
194
+ form.classList.toggle("t007-input-submit-loading", bool);
195
+ }
196
+ function toggleError(input, bool, flag = false) {
197
+ const field2 = input.closest(".t007-input-field"), floatingLabel = field2.querySelector(".t007-input-floating-label");
198
+ if (bool && flag) {
199
+ input.setAttribute("data-error", "");
200
+ floatingLabel?.classList.add("t007-input-shake");
201
+ } else if (!bool) input.removeAttribute("data-error");
202
+ toggleHelper(input, input.hasAttribute("data-error"));
203
+ }
204
+ function toggleHelper(input, bool) {
205
+ 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
+ input.closest(".t007-input-field").querySelectorAll(`.t007-input-helper-text:not([data-violation="${violation}"])`).forEach((helper2) => helper2?.classList.remove("t007-input-show"));
207
+ 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"));
216
+ }
217
+ function updatePasswordMeter(input) {
218
+ const passwordMeter = input.closest(".t007-input-field").querySelector(".t007-input-password-meter");
219
+ if (!passwordMeter) return;
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;
230
+ }
231
+ function validateInput(input, flag = false) {
232
+ if (form.dataset.globalError || !input?.classList.contains("t007-input")) return;
233
+ updatePasswordMeter(input);
234
+ let value, errorBool;
235
+ switch (input.custom ?? input.getAttribute("custom")) {
236
+ case "password":
237
+ value = input.value?.trim();
238
+ if (value === "") break;
239
+ const confirmPasswordInput = Array.from(inputs).find((input2) => (input2.custom ?? input2.getAttribute("custom")) === "confirm-password");
240
+ if (!confirmPasswordInput) break;
241
+ const confirmPasswordValue = confirmPasswordInput.value?.trim();
242
+ confirmPasswordInput.setCustomValidity(value !== confirmPasswordValue ? "Both passwords do not match" : "");
243
+ toggleError(confirmPasswordInput, value !== confirmPasswordValue, flag);
244
+ break;
245
+ case "confirm_password":
246
+ value = input.value?.trim();
247
+ if (value === "") break;
248
+ const passwordInput = Array.from(inputs).find((input2) => (input2.custom ?? input2.getAttribute("custom")) === "password");
249
+ if (!passwordInput) break;
250
+ const passwordValue = passwordInput.value?.trim();
251
+ errorBool = value !== passwordValue;
252
+ input.setCustomValidity(errorBool ? "Both passwords do not match" : "");
253
+ break;
254
+ case "onward_date":
255
+ if (input.min) break;
256
+ input.min = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
257
+ forceRevalidate(input);
258
+ break;
259
+ }
260
+ if (input.type === "file") {
261
+ 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
+ });
272
+ errorBool = !!message;
273
+ input.setCustomValidity(message);
274
+ if (violation) input.Validity[violation] = true;
275
+ }
276
+ errorBool = errorBool ?? !input.validity?.valid;
277
+ toggleError(input, errorBool, flag);
278
+ if (errorBool) return;
279
+ if (input.type === "radio")
280
+ Array.from(inputs)?.filter((i) => i.name == input.name)?.forEach((radio) => toggleError(radio, errorBool, flag));
281
+ }
282
+ function validateFormOnClient() {
283
+ Array.from(inputs).forEach((input) => validateInput(input, true));
284
+ form.querySelector("input:invalid")?.focus();
285
+ return Array.from(inputs).every((input) => input.checkValidity());
286
+ }
287
+ function toggleFormGlobalError(bool) {
288
+ form.toggleAttribute("data-global-error", bool);
289
+ form.querySelectorAll(".t007-input-field").forEach((field2) => {
290
+ field2.querySelector(".t007-input")?.toggleAttribute("data-error", bool);
291
+ if (bool) field2.querySelector(".t007-input-floating-label")?.classList.add("t007-input-shake");
292
+ });
293
+ }
294
+ }
295
+ };
296
+ var { field, handleFormValidation } = formManager;
297
+ if (typeof window !== "undefined") {
298
+ t007.FM = formManager;
299
+ t007.field = field;
300
+ t007.handleFormValidation = handleFormValidation;
301
+ window.field ??= t007.field;
302
+ window.handleFormValidation ??= t007.handleFormValidation;
303
+ console.log("%cT007 Input helpers attached to window!", "color: darkturquoise");
304
+ loadResource(T007_INPUT_CSS_SRC);
305
+ t007.FM.init();
306
+ }
307
+ export {
308
+ field,
309
+ formManager,
310
+ handleFormValidation
311
+ };
@@ -0,0 +1,441 @@
1
+ // ../utils/dist/index.js
2
+ function isSameURL(src1, src2) {
3
+ if (typeof src1 !== "string" || typeof src2 !== "string" || !src1 || !src2) return false;
4
+ try {
5
+ const u1 = new URL(src1, window.location.href), u2 = new URL(src2, window.location.href);
6
+ return decodeURIComponent(u1.origin + u1.pathname) === decodeURIComponent(u2.origin + u2.pathname);
7
+ } catch {
8
+ return src1.replace(/\\/g, "/").split("?")[0].trim() === src2.replace(/\\/g, "/").split("?")[0].trim();
9
+ }
10
+ }
11
+ function createEl(tag, props, dataset, styles, el = tag ? document?.createElement(tag) : null) {
12
+ return assignEl(el, props, dataset, styles), el;
13
+ }
14
+ function assignEl(el, props, dataset, styles) {
15
+ if (!el) return;
16
+ if (props) {
17
+ for (const k of Object.keys(props)) if (props[k] !== void 0) el[k] = props[k];
18
+ }
19
+ if (dataset) {
20
+ for (const k of Object.keys(dataset)) if (dataset[k] !== void 0) el.dataset[k] = String(dataset[k]);
21
+ }
22
+ if (styles) {
23
+ for (const k of Object.keys(styles)) if (styles[k] !== void 0) el.style[k] = styles[k];
24
+ }
25
+ }
26
+ function loadResource(src, type = "style", { module, media, crossOrigin, integrity, referrerPolicy, nonce, fetchPriority, attempts = 3, retryKey = false } = {}, w = window) {
27
+ w.t007._resourceCache ??= {};
28
+ if (w.t007._resourceCache[src]) return w.t007._resourceCache[src];
29
+ const existing = type === "script" ? Array.prototype.find.call(w.document.scripts, (s) => isSameURL(s.src, src)) : type === "style" ? Array.prototype.find.call(w.document.styleSheets, (s) => isSameURL(s.href, src)) : null;
30
+ if (existing) return w.t007._resourceCache[src] = Promise.resolve(existing);
31
+ w.t007._resourceCache[src] = new Promise((resolve, reject) => {
32
+ (function tryLoad(remaining, el) {
33
+ const onerror = () => {
34
+ el?.remove?.();
35
+ if (remaining > 1) {
36
+ setTimeout(tryLoad, 1e3, remaining - 1);
37
+ console.warn(`Retrying ${type} load (${attempts - remaining + 1}): ${src}...`);
38
+ } else {
39
+ delete w.t007._resourceCache[src];
40
+ reject(new Error(`${type} load failed after ${attempts} attempts: ${src}`));
41
+ }
42
+ };
43
+ const url = retryKey && remaining < attempts ? `${src}${src.includes("?") ? "&" : "?"}_${retryKey}=${Date.now()}` : src;
44
+ if (type === "script") w.document.body.append(el = createEl("script", { src: url, type: module ? "module" : "text/javascript", crossOrigin, integrity, referrerPolicy, nonce, fetchPriority, onload: () => resolve(el), onerror }) || "");
45
+ else if (type === "style") w.document.head.append(el = createEl("link", { rel: "stylesheet", href: url, media, crossOrigin, integrity, referrerPolicy, nonce, fetchPriority, onload: () => resolve(el), onerror }) || "");
46
+ else reject(new Error(`Unsupported resource type: ${type}`));
47
+ })(attempts);
48
+ });
49
+ return w.t007._resourceCache[src];
50
+ }
51
+ function initScrollAssist(el, { pxPerSecond = 80, assistClassName = "tmg-video-controls-scroll-assist", vertical = true, horizontal = true } = {}) {
52
+ t007._scrollers ??= /* @__PURE__ */ new WeakMap();
53
+ t007._scroller_r_observer ??= new ResizeObserver((entries) => entries.forEach(({ target }) => t007._scrollers.get(target)?.update()));
54
+ t007._scroller_m_observer ??= new MutationObserver((entries) => {
55
+ const els = /* @__PURE__ */ new Set();
56
+ for (const entry of entries) {
57
+ let node = entry.target instanceof Element ? entry.target : null;
58
+ while (node && !t007._scrollers.has(node)) node = node.parentElement;
59
+ if (node) els.add(node);
60
+ }
61
+ for (const el2 of els) t007._scrollers.get(el2)?.update();
62
+ });
63
+ const parent = el?.parentElement;
64
+ if (!parent || t007._scrollers.has(el)) return;
65
+ const assist = {};
66
+ let scrollId = null, last = performance.now(), assistWidth = 20, assistHeight = 20;
67
+ const update = () => {
68
+ const hasInteractive = !!parent.querySelector('button, a[href], input, select, textarea, [contenteditable="true"], [tabindex]:not([tabindex="-1"])');
69
+ if (horizontal) {
70
+ const w = assist.left?.offsetWidth || assistWidth, check = hasInteractive ? el.clientWidth < w * 2 : false;
71
+ assist.left.style.display = check ? "none" : el.scrollLeft > 0 ? "block" : "none";
72
+ assist.right.style.display = check ? "none" : el.scrollLeft + el.clientWidth < el.scrollWidth - 1 ? "block" : "none";
73
+ assistWidth = w;
74
+ }
75
+ if (vertical) {
76
+ const h = assist.up?.offsetHeight || assistHeight, check = hasInteractive ? el.clientHeight < h * 2 : false;
77
+ assist.up.style.display = check ? "none" : el.scrollTop > 0 ? "block" : "none";
78
+ assist.down.style.display = check ? "none" : el.scrollTop + el.clientHeight < el.scrollHeight - 1 ? "block" : "none";
79
+ assistHeight = h;
80
+ }
81
+ };
82
+ const scroll = (dir) => {
83
+ const frame = () => {
84
+ const now = performance.now(), dt = now - last;
85
+ last = now;
86
+ const d = pxPerSecond * dt / 1e3;
87
+ if (dir === "left") el.scrollLeft = Math.max(0, el.scrollLeft - d);
88
+ if (dir === "right") el.scrollLeft = Math.min(el.scrollWidth - el.clientWidth, el.scrollLeft + d);
89
+ if (dir === "up") el.scrollTop = Math.max(0, el.scrollTop - d);
90
+ if (dir === "down") el.scrollTop = Math.min(el.scrollHeight - el.clientHeight, el.scrollTop + d);
91
+ scrollId = requestAnimationFrame(frame);
92
+ };
93
+ last = performance.now();
94
+ frame();
95
+ };
96
+ const stop = () => (cancelAnimationFrame(scrollId ?? 0), scrollId = null);
97
+ const addAssist = (dir) => {
98
+ const div = createEl("div", { className: assistClassName }, { scrollDirection: dir }, { display: "none" });
99
+ if (!div) return;
100
+ ["pointerenter", "dragenter"].forEach((evt) => div.addEventListener(evt, () => scroll(dir)));
101
+ ["pointerleave", "pointerup", "pointercancel", "dragleave", "dragend"].forEach((evt) => div.addEventListener(evt, stop));
102
+ dir === "left" || dir === "up" ? parent.insertBefore(div, el) : parent.append(div);
103
+ assist[dir] = div;
104
+ };
105
+ if (horizontal) ["left", "right"].forEach(addAssist);
106
+ if (vertical) ["up", "down"].forEach(addAssist);
107
+ el.addEventListener("scroll", update);
108
+ t007._scroller_r_observer.observe(el);
109
+ t007._scroller_m_observer.observe(el, { childList: true, subtree: true, characterData: true });
110
+ t007._scrollers.set(el, {
111
+ update,
112
+ destroy() {
113
+ stop();
114
+ el.removeEventListener("scroll", update);
115
+ t007._scroller_r_observer.unobserve(el);
116
+ t007._scrollers.delete(el);
117
+ Object.values(assist).forEach((a) => a.remove());
118
+ }
119
+ });
120
+ return update(), t007._scrollers.get(el);
121
+ }
122
+ if (typeof window !== "undefined") {
123
+ window.t007 ??= {};
124
+ window.T007_TOAST_JS_SRC ??= `https://cdn.jsdelivr.net/npm/@t007/toast@latest`;
125
+ window.T007_INPUT_JS_SRC ??= `https://cdn.jsdelivr.net/npm/@t007/input@latest`;
126
+ window.T007_DIALOG_JS_SRC ??= `https://cdn.jsdelivr.net/npm/@t007/dialog@latest`;
127
+ window.T007_TOAST_CSS_SRC ??= `https://cdn.jsdelivr.net/npm/@t007/toast@latest/style.css`;
128
+ window.T007_INPUT_CSS_SRC ??= `https://cdn.jsdelivr.net/npm/@t007/input@latest/style.css`;
129
+ window.T007_DIALOG_CSS_SRC ??= `https://cdn.jsdelivr.net/npm/@t007/dialog@latest/style.css`;
130
+ }
131
+
132
+ // src/index.js
133
+ var formManager = {
134
+ forms: document.getElementsByClassName("t007-input-form"),
135
+ violationKeys: ["valueMissing", "typeMismatch", "patternMismatch", "stepMismatch", "tooShort", "tooLong", "rangeUnderflow", "rangeOverflow", "badInput", "customError"],
136
+ init() {
137
+ t007.FM.observeDOMForFields();
138
+ Array.from(t007.FM.forms).forEach(t007.FM.handleFormValidation);
139
+ },
140
+ observeDOMForFields() {
141
+ new MutationObserver((mutations) => {
142
+ for (const mutation of mutations) {
143
+ for (const node of mutation.addedNodes) {
144
+ if (!node.tagName || !(node?.classList?.contains("t007-input-field") || node?.querySelector?.(".t007-input-field"))) continue;
145
+ for (const field2 of [...node.querySelector(".t007-input-field") ? node.querySelectorAll(".t007-input-field") : [node]]) t007.FM.setUpField(field2);
146
+ }
147
+ }
148
+ }).observe(document.body, { childList: true, subtree: true });
149
+ },
150
+ getFilesHelper(files, opts) {
151
+ if (!files || !files.length) return { violation: null, message: "" };
152
+ const totalFiles = files.length;
153
+ let totalSize = 0;
154
+ let currFiles = 0;
155
+ 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)}` });
156
+ 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)}` });
157
+ for (const file of files) {
158
+ currFiles++;
159
+ totalSize += file.size;
160
+ if (opts.accept) {
161
+ const acceptedTypes = opts.accept.split(",").map((type) => type.trim().replace(/^[*\.]+|[*\.]+$/g, "")).filter(Boolean) || [];
162
+ if (!acceptedTypes.some((type) => file.type.includes(type))) return { violation: "typeMismatch", message: `File${currFiles > 1 ? currFiles : ""} type of '${file.type}' is not accepted.` };
163
+ }
164
+ if (opts.maxSize && file.size > opts.maxSize) return setMaxError(file.size, opts.maxSize, currFiles);
165
+ if (opts.minSize && file.size < opts.minSize) return setMinError(file.size, opts.minSize, currFiles);
166
+ if (opts.multiple) {
167
+ if (opts.maxTotalSize && totalSize > opts.maxTotalSize) return setMaxError(totalSize, opts.maxTotalSize);
168
+ if (opts.minTotalSize && totalSize < opts.minTotalSize) return setMinError(totalSize, opts.minTotalSize);
169
+ 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"}` };
170
+ 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"}` };
171
+ }
172
+ }
173
+ return { violation: null, message: "" };
174
+ },
175
+ formatSize(size, decimals = 3, base = 1e3) {
176
+ if (size < base) return `${size} byte${size == 1 ? "" : "s"}`;
177
+ 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);
178
+ return `${(size / Math.pow(base, exponent)).toFixed(decimals).replace(/\.0+$/, "")} ${units[exponent]}`;
179
+ },
180
+ togglePasswordType: (input) => input.type = input.type === "password" ? "text" : "password",
181
+ toggleFilled: (input) => input?.toggleAttribute("data-filled", input.type === "checkbox" || input.type === "radio" ? input.checked : input.value !== "" || input.files?.length > 0),
182
+ setFallbackHelper(field2) {
183
+ const helperTextWrapper = field2?.querySelector(".t007-input-helper-text-wrapper");
184
+ if (!helperTextWrapper || helperTextWrapper.querySelector(".t007-input-helper-text[data-violation='auto']")) return;
185
+ helperTextWrapper.append(createEl("p", { className: "t007-input-helper-text" }, { violation: "auto" }));
186
+ },
187
+ setFieldListeners(field2) {
188
+ if (!field2) return;
189
+ const input = field2.querySelector(".t007-input"), floatingLabel = field2.querySelector(".t007-input-floating-label"), eyeOpen = field2.querySelector(".t007-input-password-visible-icon"), eyeClosed = field2.querySelector(".t007-input-password-hidden-icon");
190
+ if (input.type === "file")
191
+ input.addEventListener("input", async () => {
192
+ const file = input.files?.[0], img = new Image();
193
+ img.onload = () => {
194
+ input.style.setProperty("--t007-input-image-src", `url(${src})`);
195
+ input.classList.add("t007-input-image-selected");
196
+ setTimeout(() => URL.revokeObjectURL(src), 1e3);
197
+ };
198
+ img.onerror = () => {
199
+ input.style.removeProperty("--t007-input-image-src");
200
+ input.classList.remove("t007-input-image-selected");
201
+ URL.revokeObjectURL(src);
202
+ };
203
+ let src;
204
+ if (file?.type?.startsWith("image")) src = URL.createObjectURL(file);
205
+ else if (file?.type?.startsWith("video")) {
206
+ src = await new Promise((resolve) => {
207
+ let video = createEl("video"), canvas = createEl("canvas"), context = canvas.getContext("2d");
208
+ video.ontimeupdate = () => {
209
+ context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
210
+ canvas.toBlob((blob) => resolve(URL.createObjectURL(blob)));
211
+ URL.revokeObjectURL(video.src);
212
+ video = video.src = video.onloadedmetadata = video.ontimeupdate = null;
213
+ };
214
+ video.onloadeddata = () => video.currentTime = 3;
215
+ video.src = URL.createObjectURL(file);
216
+ });
217
+ }
218
+ if (!src) {
219
+ input.style.removeProperty("--t007-input-image-src");
220
+ input.classList.remove("t007-input-image-selected");
221
+ return;
222
+ }
223
+ img.src = src;
224
+ });
225
+ if (floatingLabel) floatingLabel.ontransitionend = () => floatingLabel.classList.remove("t007-input-shake");
226
+ if (eyeOpen && eyeClosed) eyeOpen.onclick = eyeClosed.onclick = () => t007.FM.togglePasswordType(input);
227
+ initScrollAssist(field2.querySelector(".t007-input-helper-text-wrapper"), { vertical: false });
228
+ },
229
+ setUpField(field2) {
230
+ if (field2.dataset.setUp) return;
231
+ t007.FM.toggleFilled(field2.querySelector(".t007-input"));
232
+ t007.FM.setFallbackHelper(field2);
233
+ t007.FM.setFieldListeners(field2);
234
+ field2.dataset.setUp = "true";
235
+ },
236
+ 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 }) {
237
+ const isSelect = type === "select", isTextArea = type === "textarea", isCheckboxOrRadio = type === "checkbox" || type === "radio", field2 = createEl("div", { className: `t007-input-field${isWrapper ? " t007-input-is-wrapper" : ""}${indeterminate ? " t007-input-indeterminate" : ""}${!!nativeIcon ? " t007-input-icon-override" : ""}${helperText === false ? " t007-input-no-helper" : ""}${fieldClassName ? ` ${fieldClassName}` : ""}` }), labelEl = createEl("label", { className: isCheckboxOrRadio ? `t007-input-${type}-wrapper` : "t007-input-wrapper" });
238
+ field2.append(labelEl);
239
+ if (isCheckboxOrRadio) {
240
+ labelEl.innerHTML = `
241
+ <span class="t007-input-${type}-box">
242
+ <span class="t007-input-${type}-tag"></span>
243
+ </span>
244
+ <span class="t007-input-${type}-label">${label}</span>
245
+ `;
246
+ } else {
247
+ const outline = createEl("span", { className: "t007-input-outline" });
248
+ outline.innerHTML = `
249
+ <span class="t007-input-outline-leading"></span>
250
+ <span class="t007-input-outline-notch">
251
+ <span class="t007-input-floating-label">${label}</span>
252
+ </span>
253
+ <span class="t007-input-outline-trailing"></span>
254
+ `;
255
+ labelEl.append(outline);
256
+ }
257
+ const inputEl = field2.inputEl = createEl(isTextArea ? "textarea" : isSelect ? "select" : "input", { className: `t007-input${className ? ` ${className}` : ""}`, placeholder });
258
+ if (isSelect && Array.isArray(options)) inputEl.innerHTML = options.map((opt) => typeof opt === "string" ? `<option value="${opt}">${opt}</option>` : `<option value="${opt.value}">${opt.option}</option>`).join("");
259
+ if (!isSelect && !isTextArea) inputEl.type = type;
260
+ if (custom) inputEl.setAttribute("custom", custom);
261
+ if (minSize) inputEl.setAttribute("minsize", minSize);
262
+ if (maxSize) inputEl.setAttribute("maxsize", maxSize);
263
+ if (minTotalSize) inputEl.setAttribute("mintotalsize", minTotalSize);
264
+ if (maxTotalSize) inputEl.setAttribute("maxtotalsize", maxTotalSize);
265
+ Object.keys(otherProps).forEach((key) => inputEl[key] = otherProps[key]);
266
+ labelEl.append(!isWrapper ? inputEl : children);
267
+ const nativeTypes = ["date", "time", "month", "datetime-local"];
268
+ if (nativeTypes.includes(type) && nativeIcon) labelEl.append(createEl("i", { className: "t007-input-icon t007-input-native-icon", innerHTML: nativeIcon }));
269
+ else if (endIcon) labelEl.append(createEl("i", { className: "t007-input-icon", innerHTML: endIcon }));
270
+ if (type === "password" && eyeToggler) {
271
+ labelEl.append(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>` }));
272
+ labelEl.append(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>` }));
273
+ }
274
+ if (helperText !== false) {
275
+ const helperLine = createEl("div", { className: "t007-input-helper-line" }), helperWrapper = createEl("div", { className: "t007-input-helper-text-wrapper", tabIndex: "-1" });
276
+ if (helperText.info) helperWrapper.append(createEl("p", { className: "t007-input-helper-text", textContent: helperText.info }, { violation: "none" }));
277
+ t007.FM?.violationKeys?.forEach((key) => helperText[key] && helperWrapper.append(createEl("p", { className: "t007-input-helper-text", textContent: helperText[key] }, { violation: key })));
278
+ helperLine.append(helperWrapper);
279
+ field2.append(helperLine);
280
+ }
281
+ if (passwordMeter && type === "password") {
282
+ const meter = createEl("div", { className: "t007-input-password-meter" }, { strengthLevel: "1" });
283
+ meter.innerHTML = `
284
+ <div class="t007-input-password-strength-meter">
285
+ <div class="t007-input-p-weak"></div>
286
+ <div class="t007-input-p-fair"></div>
287
+ <div class="t007-input-p-strong"></div>
288
+ <div class="t007-input-p-very-strong"></div>
289
+ </div>
290
+ `;
291
+ field2.append(meter);
292
+ }
293
+ return field2;
294
+ },
295
+ handleFormValidation(form) {
296
+ if (!form?.classList.contains("t007-input-form") || form.dataset?.isValidating) return;
297
+ form.dataset.isValidating = "true";
298
+ form.validateOnClient = validateFormOnClient;
299
+ form.toggleGlobalError = toggleFormGlobalError;
300
+ const fields = form.getElementsByClassName("t007-input-field"), inputs = form.getElementsByClassName("t007-input");
301
+ Array.from(fields).forEach(t007.FM.setUpField);
302
+ form.addEventListener("input", ({ target }) => {
303
+ t007.FM.toggleFilled(target);
304
+ validateInput(target);
305
+ });
306
+ form.addEventListener("focusout", ({ target }) => validateInput(target, true));
307
+ form.addEventListener("submit", async (e) => {
308
+ toggleSubmitLoader(true);
309
+ try {
310
+ e.preventDefault();
311
+ if (!validateFormOnClient()) return;
312
+ if (form.validateOnServer && !await form.validateOnServer()) {
313
+ toggleFormGlobalError(true);
314
+ form.addEventListener("input", () => toggleFormGlobalError(false), { once: true, useCapture: true });
315
+ return;
316
+ }
317
+ form.onSubmit ? form.onSubmit() : form.submit();
318
+ } catch (error) {
319
+ console.error(error);
320
+ }
321
+ toggleSubmitLoader(false);
322
+ });
323
+ function toggleSubmitLoader(bool) {
324
+ form.classList.toggle("t007-input-submit-loading", bool);
325
+ }
326
+ function toggleError(input, bool, flag = false) {
327
+ const field2 = input.closest(".t007-input-field"), floatingLabel = field2.querySelector(".t007-input-floating-label");
328
+ if (bool && flag) {
329
+ input.setAttribute("data-error", "");
330
+ floatingLabel?.classList.add("t007-input-shake");
331
+ } else if (!bool) input.removeAttribute("data-error");
332
+ toggleHelper(input, input.hasAttribute("data-error"));
333
+ }
334
+ function toggleHelper(input, bool) {
335
+ 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"]`);
336
+ input.closest(".t007-input-field").querySelectorAll(`.t007-input-helper-text:not([data-violation="${violation}"])`).forEach((helper2) => helper2?.classList.remove("t007-input-show"));
337
+ if (helper) helper.classList.toggle("t007-input-show", bool);
338
+ else if (fallbackHelper) {
339
+ fallbackHelper.textContent = input.validationMessage;
340
+ fallbackHelper.classList.toggle("t007-input-show", bool);
341
+ }
342
+ }
343
+ function forceRevalidate(input) {
344
+ input.checkValidity();
345
+ input.dispatchEvent(new Event("input"));
346
+ }
347
+ function updatePasswordMeter(input) {
348
+ const passwordMeter = input.closest(".t007-input-field").querySelector(".t007-input-password-meter");
349
+ if (!passwordMeter) return;
350
+ const value = input.value?.trim();
351
+ let strengthLevel = 0;
352
+ if (value.length < Number(input.minLength ?? 0)) strengthLevel = 1;
353
+ else {
354
+ if (/[a-z]/.test(value)) strengthLevel++;
355
+ if (/[A-Z]/.test(value)) strengthLevel++;
356
+ if (/[0-9]/.test(value)) strengthLevel++;
357
+ if (/[\W_]/.test(value)) strengthLevel++;
358
+ }
359
+ passwordMeter.dataset.strengthLevel = strengthLevel;
360
+ }
361
+ function validateInput(input, flag = false) {
362
+ if (form.dataset.globalError || !input?.classList.contains("t007-input")) return;
363
+ updatePasswordMeter(input);
364
+ let value, errorBool;
365
+ switch (input.custom ?? input.getAttribute("custom")) {
366
+ case "password":
367
+ value = input.value?.trim();
368
+ if (value === "") break;
369
+ const confirmPasswordInput = Array.from(inputs).find((input2) => (input2.custom ?? input2.getAttribute("custom")) === "confirm-password");
370
+ if (!confirmPasswordInput) break;
371
+ const confirmPasswordValue = confirmPasswordInput.value?.trim();
372
+ confirmPasswordInput.setCustomValidity(value !== confirmPasswordValue ? "Both passwords do not match" : "");
373
+ toggleError(confirmPasswordInput, value !== confirmPasswordValue, flag);
374
+ break;
375
+ case "confirm_password":
376
+ value = input.value?.trim();
377
+ if (value === "") break;
378
+ const passwordInput = Array.from(inputs).find((input2) => (input2.custom ?? input2.getAttribute("custom")) === "password");
379
+ if (!passwordInput) break;
380
+ const passwordValue = passwordInput.value?.trim();
381
+ errorBool = value !== passwordValue;
382
+ input.setCustomValidity(errorBool ? "Both passwords do not match" : "");
383
+ break;
384
+ case "onward_date":
385
+ if (input.min) break;
386
+ input.min = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
387
+ forceRevalidate(input);
388
+ break;
389
+ }
390
+ if (input.type === "file") {
391
+ input.Validity = {};
392
+ const { violation, message } = t007.FM.getFilesHelper(input.files ?? [], {
393
+ accept: input.accept,
394
+ multiple: input.multiple,
395
+ maxSize: input.maxSize ?? Number(input.getAttribute("maxsize")),
396
+ minSize: input.minSize ?? Number(input.getAttribute("minsize")),
397
+ maxTotalSize: input.maxTotalSize ?? Number(input.getAttribute("maxtotalsize")),
398
+ minTotalSize: input.minTotalSize ?? Number(input.getAttribute("mintotalsize")),
399
+ maxLength: input.maxLength ?? Number(input.getAttribute("maxlength")),
400
+ minLength: input.minLength ?? Number(input.getAttribute("minLength"))
401
+ });
402
+ errorBool = !!message;
403
+ input.setCustomValidity(message);
404
+ if (violation) input.Validity[violation] = true;
405
+ }
406
+ errorBool = errorBool ?? !input.validity?.valid;
407
+ toggleError(input, errorBool, flag);
408
+ if (errorBool) return;
409
+ if (input.type === "radio")
410
+ Array.from(inputs)?.filter((i) => i.name == input.name)?.forEach((radio) => toggleError(radio, errorBool, flag));
411
+ }
412
+ function validateFormOnClient() {
413
+ Array.from(inputs).forEach((input) => validateInput(input, true));
414
+ form.querySelector("input:invalid")?.focus();
415
+ return Array.from(inputs).every((input) => input.checkValidity());
416
+ }
417
+ function toggleFormGlobalError(bool) {
418
+ form.toggleAttribute("data-global-error", bool);
419
+ form.querySelectorAll(".t007-input-field").forEach((field2) => {
420
+ field2.querySelector(".t007-input")?.toggleAttribute("data-error", bool);
421
+ if (bool) field2.querySelector(".t007-input-floating-label")?.classList.add("t007-input-shake");
422
+ });
423
+ }
424
+ }
425
+ };
426
+ var { field, handleFormValidation } = formManager;
427
+ if (typeof window !== "undefined") {
428
+ t007.FM = formManager;
429
+ t007.field = field;
430
+ t007.handleFormValidation = handleFormValidation;
431
+ window.field ??= t007.field;
432
+ window.handleFormValidation ??= t007.handleFormValidation;
433
+ console.log("%cT007 Input helpers attached to window!", "color: darkturquoise");
434
+ loadResource(T007_INPUT_CSS_SRC);
435
+ t007.FM.init();
436
+ }
437
+ export {
438
+ field,
439
+ formManager,
440
+ handleFormValidation
441
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@t007/input",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "A lightweight, pure JS input system.",
5
5
  "author": "Oketade Oluwatobiloba <tobioketade007@gmail.com>",
6
6
  "license": "MIT",