@jdlien/validator-utils 1.1.1 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/validator-utils.d.ts +56 -0
- package/dist/validator-utils.js +570 -1
- package/package.json +3 -2
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilities used by the Validator class.
|
|
3
|
+
*
|
|
4
|
+
* @format
|
|
5
|
+
*/
|
|
6
|
+
type DateParts = {
|
|
7
|
+
year: number;
|
|
8
|
+
month: number;
|
|
9
|
+
day: number;
|
|
10
|
+
};
|
|
11
|
+
export declare function isFormControl(el: any): boolean;
|
|
12
|
+
interface ValidationResult {
|
|
13
|
+
valid: boolean;
|
|
14
|
+
error?: boolean;
|
|
15
|
+
messages: string[];
|
|
16
|
+
}
|
|
17
|
+
export declare function isType(el: HTMLInputElement | HTMLTextAreaElement, types: string | string[]): boolean;
|
|
18
|
+
export declare function momentToFPFormat(format: string): string;
|
|
19
|
+
export declare function monthToNumber(str: string | number): number;
|
|
20
|
+
export declare function yearToFull(year: number | string): number;
|
|
21
|
+
export declare function parseDate(value: string | Date): Date;
|
|
22
|
+
export declare function guessDatePart(num: number, knownMeanings?: (string | null)[]): string[];
|
|
23
|
+
export declare function guessDateParts(str: string): DateParts;
|
|
24
|
+
export declare function parseTime(value: string): {
|
|
25
|
+
hour: number;
|
|
26
|
+
minute: number;
|
|
27
|
+
second: number;
|
|
28
|
+
} | null;
|
|
29
|
+
export declare function parseTimeToString(value: string, format?: string): string;
|
|
30
|
+
export declare function formatDateTime(date: Date | string, format?: string): string;
|
|
31
|
+
export declare function parseDateToString(value: string | Date, format?: string): string;
|
|
32
|
+
export declare function isDate(value: string | Date): boolean;
|
|
33
|
+
export declare function isDateInRange(date: Date, range: string): boolean;
|
|
34
|
+
export declare function isTime(value: string): boolean;
|
|
35
|
+
export declare function isEmail(value: string): boolean;
|
|
36
|
+
export declare function parseNANPTel(value: string): string;
|
|
37
|
+
export declare function isNANPTel(value: string): boolean;
|
|
38
|
+
export declare function parseInteger(value: string): string;
|
|
39
|
+
export declare function isNumber(value: string): boolean;
|
|
40
|
+
export declare function parseNumber(value: string): string;
|
|
41
|
+
export declare function isInteger(value: string): boolean;
|
|
42
|
+
export declare function parseUrl(value: string): string;
|
|
43
|
+
export declare function isUrl(value: string): boolean;
|
|
44
|
+
export declare function parseZip(value: string): string;
|
|
45
|
+
export declare function isZip(value: string): boolean;
|
|
46
|
+
export declare function parsePostalCA(value: string): string;
|
|
47
|
+
export declare function isPostalCA(value: string): boolean;
|
|
48
|
+
export declare function isColor(value: string): boolean;
|
|
49
|
+
export declare function parseColor(value: string): string;
|
|
50
|
+
export declare function normalizeValidationResult(res: boolean | string | {
|
|
51
|
+
valid: boolean;
|
|
52
|
+
message?: string;
|
|
53
|
+
messages?: string | string[];
|
|
54
|
+
error?: boolean;
|
|
55
|
+
}): ValidationResult;
|
|
56
|
+
export {};
|
package/dist/validator-utils.js
CHANGED
|
@@ -1 +1,570 @@
|
|
|
1
|
-
(function(i,f){typeof exports=="object"&&typeof module<"u"?f(exports):typeof define=="function"&&define.amd?define(["exports"],f):(i=typeof globalThis<"u"?globalThis:i||self,f(i.validatorUtils={}))})(this,function(i){"use strict";function f(e){return e instanceof HTMLInputElement||e instanceof HTMLSelectElement||e instanceof HTMLTextAreaElement}function R(e,n){typeof n=="string"&&(n=[n]);const t=e.dataset.type||"",r=e.type;return!!(n.includes(t)||n.includes(r))}function T(e){return e.replace(/YYYY/g,"Y").replace(/YY/g,"y").replace(/MMMM/g,"F").replace(/MMM/g,"{3}").replace(/MM/g,"{2}").replace(/M/g,"n").replace(/DD/g,"{5}").replace(/D/g,"j").replace(/dddd/g,"l").replace(/ddd/g,"D").replace(/dd/g,"D").replace(/d/g,"w").replace(/HH/g,"{6}").replace(/H/g,"G").replace(/hh/g,"h").replace(/mm/g,"i").replace(/m/g,"i").replace(/ss/g,"S").replace(/s/g,"s").replace(/A/gi,"K").replace(/\{3\}/g,"M").replace(/\{2\}/g,"m").replace(/\{5\}/g,"d").replace(/\{6\}/g,"H")}function M(e){const n=parseInt(e);if(typeof e=="number"||!isNaN(n))return n-1;const t=new Date(`1 ${e} 2000`).getMonth();if(!isNaN(t))return t;const r={ja:0,en:0,fe:1,fé:1,ap:3,ab:3,av:3,mai:4,juin:5,juil:6,au:7,ag:7,ao:7,se:8,o:9,n:10,d:11};for(const l in r)if(e.toLowerCase().startsWith(l))return r[l];throw new Error("Invalid month name: "+e)}function y(e){return typeof e=="string"&&(e=parseInt(e.replace(/\D/g,""))),e>99?e:e<(new Date().getFullYear()+20)%100?e+2e3:e+1900}function m(e){if(e instanceof Date)return e;e=e.trim().toLowerCase();let n=0,t=0,r=0,l=0,s=0,a=0;const d=new RegExp(/\d{1,2}\:\d\d(?:\:\d\ds?)?\s?(?:[a|p]m?)?/gi);if(d.test(e)){const c=e.match(d)[0];e=e.replace(c,"").trim();const g=p(c);if(g!==null&&({hour:l,minute:s,second:a}=g),e.length<=2){const D=new Date;return new Date(D.getFullYear(),D.getMonth(),D.getDate(),l,s,a)}}const u=/(^|\b)(mo|tu|we|th|fr|sa|su|lu|mard|mer|jeu|ve|dom)[\w]*\.?/gi;e=e.replace(u,"").trim();const o=new Date(new Date().setHours(0,0,0,0));if(/(now|today)/.test(e))return o;if(e.includes("tomorrow"))return new Date(o.setDate(o.getDate()+1));e.length===8&&(e=e.replace(/(\d\d\d\d)(\d\d)(\d\d)/,"$1-$2-$3")),e.length===6&&(e=e.replace(/(\d\d)(\d\d)(\d\d)/,y(e.slice(0,2))+"-$2-$3"));try{({year:n,month:t,day:r}=S(e))}catch{return new Date("")}return new Date(n,t-1,r,l,s,a)}function N(e,n=[null,null,null]){const t=r=>r.filter(l=>!n.includes(l));return e===0||e>31?t(["y"]):e>12?t(["d","y"]):e>=1&&e<=12?t(["m","d","y"]):[]}function S(e){const n=e.split(/[\s-/:.,]+/).filter(s=>s!=="");if(n.length<3){if(e.match(/\d{4}/)!==null)throw new Error("Invalid Date");n.unshift(String(new Date().getFullYear()))}const t={year:0,month:0,day:0};function r(s,a){s==="year"?t.year=y(a):t[s]=a}let l=0;for(;!(t.year&&t.month&&t.day);){e:for(const s of n){if(l++,/^[a-zA-Zé]+$/.test(s)){t.month||r("month",M(s)+1);continue}if(/^'\d\d$/.test(s)||/^\d{3,5}$/.test(s)){t.year||r("year",parseInt(s.replace(/'/,"")));continue}const a=parseInt(s);if(isNaN(a))throw console.error(`not date because ${s} isNaN`),new Error("Invalid Date");const d=N(a,[t.year?"y":null,t.month?"m":null,t.day?"d":null]);if(d.length==1){if(d[0]==="m"&&!t.month){r("month",a);continue e}if(d[0]==="d"&&!t.day){r("day",a);continue e}if(d[0]==="y"&&!t.year){r("year",a);continue e}}l>3&&(!t.month&&d.includes("m")?r("month",a):!t.day&&d.includes("d")&&r("day",a))}if(l>6)throw new Error("Invalid Date")}if(t.year&&t.month&&t.day)return t;throw new Error("Invalid Date")}function p(e){if(e=e.trim().toLowerCase(),e==="now"){const o=new Date;return{hour:o.getHours(),minute:o.getMinutes(),second:o.getSeconds()}}const n=e.match(/(\d{3,4})/);if(n){const o=n[1].length,c=n[1].slice(0,o==3?1:2),g=n[1].slice(-2);e=e.replace(n[1],c+":"+g)}const t=new RegExp(/^(\d{1,2})(?::(\d{1,2}))?\s*(?:(a|p)m?)?$/i);if(t.test(e)){const o=e.match(t);if(o===null)return null;e=o[1]+":"+(o[2]||"00")+(o[3]||"")}const r=new RegExp(/^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?\s*(?:(a|p)m?)?$/i);if(!r.test(e))return null;const l=e.match(r);if(l===null)return null;const s=parseInt(l[1]),a=parseInt(l[2]),d=l[3]?parseInt(l[3]):0,u=l[4];return isNaN(s)||isNaN(a)||isNaN(d)?null:u==="p"&&s<12?{hour:s+12,minute:a,second:d}:u==="a"&&s===12?{hour:0,minute:a,second:d}:s<0||s>23||a<0||a>59||d<0||d>59?null:{hour:s,minute:a,second:d}}function k(e,n="h:mm A"){const t=p(e);if(t){const r=new Date;return r.setHours(t.hour),r.setMinutes(t.minute),r.setSeconds(t.second),r.setMilliseconds(0),w(r,n)}return""}function w(e,n="YYYY-MM-DD"){if(e=m(e),isNaN(e.getTime()))return"";const t={y:e.getFullYear(),M:e.getMonth(),D:e.getDate(),W:e.getDay(),H:e.getHours(),m:e.getMinutes(),s:e.getSeconds(),ms:e.getMilliseconds()},r=(o,c=2)=>(o+"").padStart(c,"0"),l=()=>t.H%12||12,s=o=>o<12?"AM":"PM",a=o=>"January|February|March|April|May|June|July|August|September|October|November|December".split("|")[o];function d(o,c=0){const g="Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday".split("|");return c?g[o].slice(0,c):g[o]}const u={YY:String(t.y).slice(-2),YYYY:t.y,M:t.M+1,MM:r(t.M+1),MMMM:a(t.M),MMM:a(t.M).slice(0,3),D:String(t.D),DD:r(t.D),d:String(t.W),dd:d(t.W,2),ddd:d(t.W,3),dddd:d(t.W),H:String(t.H),HH:r(t.H),h:l(),hh:r(l()),A:s(t.H),a:s(t.H).toLowerCase(),m:String(t.m),mm:r(t.m),s:String(t.s),ss:r(t.s),SSS:r(t.ms,3)};return n.replace(/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,(o,c)=>c||u[o])}function $(e,n){const t=m(e);return isNaN(t.getTime())?"":((!n||n.length===0)&&(n="YYYY-MMM-DD"),w(t,n))}function x(e){if(typeof e!="string"&&!(e instanceof Date))return!1;let n=m(e);return n==null?!1:!isNaN(n.getTime())}function A(e,n){return!(n==="past"&&e>new Date||n==="future"&&e.getTime()<new Date().setHours(0,0,0,0))}function E(e){let n=p(e);return n===null?!1:!isNaN(n.hour)&&!isNaN(n.minute)&&!isNaN(n.second)}function C(e){if(e.length>255||!new RegExp(/^.+@.+\.[a-zA-Z0-9]{2,}$/).test(e))return!1;let t="";return t+="^([a-zA-Z0-9!#$%'*+/=?^_`{|}~-]+",t+="(?:\\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*",t+="|",t+='"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*"',t+=")@(",t+="(",t+="(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\\.)+",t+="[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?",t+=")",t+=")$",new RegExp(t).test(e)}function Y(e){return e=e.replace(/^[^2-90]+/g,""),e=e.replace(/(\d\d\d).*?(\d\d\d).*?(\d\d\d\d)(.*)/,"$1-$2-$3$4"),e}function H(e){return/^\d\d\d-\d\d\d-\d\d\d\d$/.test(e)}function P(e){return e.replace(/[^0-9]/g,"")}function I(e){return/^\-?\d*\.?\d*$/.test(e)}function Z(e){return e.replace(/[^\-0-9.]/g,"").replace(/(^-)|(-)/g,(n,t)=>t?"-":"").replace(/(\..*)\./g,"$1")}function z(e){return/^\-?\d*$/.test(e)}function F(e){return e=e.trim(),new RegExp("^(?:[a-z+]+:)?//","i").test(e)?e:"https://"+e}function L(e){return new RegExp("^(?:[-a-z+]+:)?//","i").test(e)}function j(e){return e=e.replace(/[^0-9]/g,"").replace(/(.{5})(.*)/,"$1-$2").trim(),e.length===6&&(e=e.replace(/-/,"")),e}function q(e){return new RegExp(/^\d{5}(-\d{4})?$/).test(e)}function W(e){return e=e.toUpperCase().replace(/[^A-Z0-9]/g,"").replace(/(.{3})\s*(.*)/,"$1 $2").trim(),e}function J(e){return new RegExp(/^[ABCEGHJKLMNPRSTVXY][0-9][ABCEGHJKLMNPRSTVWXYZ] ?[0-9][ABCEGHJKLMNPRSTVWXYZ][0-9]$/).test(e)}function U(e){return["transparent","currentColor"].includes(e)?!0:typeof e!="string"||!e.trim()?!1:typeof CSS=="object"&&typeof CSS.supports=="function"?CSS.supports("color",e):V(e)}function V(e){const n=new RegExp(/^rgba?\(\s*(\d{1,3}%?,\s*){2}\d{1,3}%?\s*(?:,\s*(\.\d+|0+(\.\d+)?|1(\.0+)?|0|1\.0|\d{1,2}(\.\d*)?%|100%))?\s*\)$/),t=new RegExp(/^hsla?\(\s*\d+(deg|grad|rad|turn)?,\s*\d{1,3}%,\s*\s*\d{1,3}%(?:,\s*(\.\d+|0+(\.\d+)?|1(\.0+)?|0|1\.0|\d{1,2}(\.\d*)?%|100%))?\s*\)$/),r=new RegExp(/^rgba?\(\s*(\d{1,3}%?\s+){2}\d{1,3}%?\s*(?:\s*\/\s*(\.\d+|0+(\.\d+)?|1(\.0+)?|0|1\.0|\d{1,2}(\.\d*)?%|100%))?\s*\)$/),l=new RegExp(/^hsla?\(\s*\d+(deg|grad|rad|turn)?\s+\d{1,3}%\s+\s*\d{1,3}%(?:\s*\/\s*(\.\d+|0+(\.\d+)?|1(\.0+)?|0|1\.0|\d{1,2}(\.\d*)?%|100%))?\s*\)$/),s=new RegExp(/^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i);let a="aliceblue|antiquewhite|aqua|aquamarine|azure|beige|bisque|black|blanchedalmond|blue|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|cyan|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkgrey|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|firebrick|floralwhite|forestgreen|fuchsia|gainsboro|ghostwhite|gold|goldenrod|gray|green|greenyellow|grey|honeydew|hotpink|indianred|indigo|ivory|khaki|lavender|lavenderblush|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgray|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightslategrey|lightsteelblue|lightyellow|lime|limegreen|linen|magenta|maroon|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|navy|oldlace|olive|olivedrab|orange|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|purple|rebeccapurple|red|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|silver|skyblue|slateblue|slategray|slategrey|snow|springgreen|steelblue|tan|teal|thistle|tomato|turquoise|violet|wheat|white|whitesmoke|yellow|yellowgreen";const d=new RegExp(`^(${a})$`,"i");return n.test(e)||t.test(e)||r.test(e)||l.test(e)||s.test(e)||d.test(e)}let h=null;const b=new Map;function G(e){if(e=e.trim().toLowerCase(),["transparent","currentcolor"].includes(e))return e;if(b.has(e))return b.get(e);h===null&&(h=document.createElement("canvas"),h.willReadFrequently=!0);let n=h.getContext("2d");if(!n)throw new Error("Can't get context from colorCanvas");n.fillStyle=e,n.fillRect(0,0,1,1);let t=n.getImageData(0,0,1,1).data,r="#"+("000000"+(t[0]<<16|t[1]<<8|t[2]).toString(16)).slice(-6);return b.set(e,r),r}function K(e){let n={valid:!1,error:!1,messages:[]};return typeof e=="boolean"?{valid:e,error:!1,messages:[]}:typeof e=="string"?{valid:!1,error:!1,messages:[e]}:(typeof e.valid=="boolean"&&(n.valid=e.valid),typeof e.message=="string"&&(n.messages=[e.message]),typeof e.messages=="string"&&(n.messages=[e.messages]),Array.isArray(e.messages)&&(n.messages=e.messages),e.error===!0&&(n.error=!0),n)}i.formatDateTime=w,i.isColor=U,i.isDate=x,i.isDateInRange=A,i.isEmail=C,i.isFormControl=f,i.isInteger=z,i.isNANPTel=H,i.isNumber=I,i.isPostalCA=J,i.isTime=E,i.isType=R,i.isUrl=L,i.isZip=q,i.momentToFPFormat=T,i.monthToNumber=M,i.normalizeValidationResult=K,i.parseColor=G,i.parseDate=m,i.parseDateToString=$,i.parseInteger=P,i.parseNANPTel=Y,i.parseNumber=Z,i.parsePostalCA=W,i.parseTime=p,i.parseTimeToString=k,i.parseUrl=F,i.parseZip=j,i.yearToFull=y,Object.defineProperty(i,Symbol.toStringTag,{value:"Module"})});
|
|
1
|
+
/**
|
|
2
|
+
* Utilities used by the Validator class.
|
|
3
|
+
*
|
|
4
|
+
* @format
|
|
5
|
+
*/
|
|
6
|
+
export function isFormControl(el) {
|
|
7
|
+
return (el instanceof HTMLInputElement ||
|
|
8
|
+
el instanceof HTMLSelectElement ||
|
|
9
|
+
el instanceof HTMLTextAreaElement);
|
|
10
|
+
}
|
|
11
|
+
// Checks if an element has a type or data-type attribute matching
|
|
12
|
+
// one of the types in the passed array
|
|
13
|
+
export function isType(el, types) {
|
|
14
|
+
if (typeof types === 'string')
|
|
15
|
+
types = [types];
|
|
16
|
+
const dataType = el.dataset.type || '';
|
|
17
|
+
const type = el.type;
|
|
18
|
+
if (types.includes(dataType))
|
|
19
|
+
return true;
|
|
20
|
+
if (types.includes(type))
|
|
21
|
+
return true;
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
// Converts moment.js-style formats strings to the flatpickr format
|
|
25
|
+
// https://flatpickr.js.org/formatting/
|
|
26
|
+
// Useful to use FlatPickr in conjunction with Validator
|
|
27
|
+
// Not comprehensive but covers common format strings
|
|
28
|
+
export function momentToFPFormat(format) {
|
|
29
|
+
return format
|
|
30
|
+
.replace(/YYYY/g, 'Y')
|
|
31
|
+
.replace(/YY/g, 'y')
|
|
32
|
+
.replace(/MMMM/g, 'F')
|
|
33
|
+
.replace(/MMM/g, '{3}')
|
|
34
|
+
.replace(/MM/g, '{2}')
|
|
35
|
+
.replace(/M/g, 'n')
|
|
36
|
+
.replace(/DD/g, '{5}')
|
|
37
|
+
.replace(/D/g, 'j')
|
|
38
|
+
.replace(/dddd/g, 'l')
|
|
39
|
+
.replace(/ddd/g, 'D')
|
|
40
|
+
.replace(/dd/g, 'D')
|
|
41
|
+
.replace(/d/g, 'w')
|
|
42
|
+
.replace(/HH/g, '{6}')
|
|
43
|
+
.replace(/H/g, 'G')
|
|
44
|
+
.replace(/hh/g, 'h')
|
|
45
|
+
.replace(/mm/g, 'i')
|
|
46
|
+
.replace(/m/g, 'i')
|
|
47
|
+
.replace(/ss/g, 'S')
|
|
48
|
+
.replace(/s/g, 's')
|
|
49
|
+
.replace(/A/gi, 'K')
|
|
50
|
+
.replace(/\{3\}/g, 'M')
|
|
51
|
+
.replace(/\{2\}/g, 'm')
|
|
52
|
+
.replace(/\{5\}/g, 'd')
|
|
53
|
+
.replace(/\{6\}/g, 'H');
|
|
54
|
+
}
|
|
55
|
+
// Converts month name to zero-based month number
|
|
56
|
+
export function monthToNumber(str) {
|
|
57
|
+
const num = parseInt(str);
|
|
58
|
+
if (typeof str === 'number' || !isNaN(num))
|
|
59
|
+
return num - 1;
|
|
60
|
+
const m = new Date(`1 ${str} 2000`).getMonth();
|
|
61
|
+
if (!isNaN(m))
|
|
62
|
+
return m;
|
|
63
|
+
const dict = {
|
|
64
|
+
ja: 0,
|
|
65
|
+
en: 0,
|
|
66
|
+
fe: 1,
|
|
67
|
+
fé: 1,
|
|
68
|
+
ap: 3,
|
|
69
|
+
ab: 3,
|
|
70
|
+
av: 3,
|
|
71
|
+
mai: 4,
|
|
72
|
+
juin: 5,
|
|
73
|
+
juil: 6,
|
|
74
|
+
au: 7,
|
|
75
|
+
ag: 7,
|
|
76
|
+
ao: 7,
|
|
77
|
+
se: 8,
|
|
78
|
+
o: 9,
|
|
79
|
+
n: 10,
|
|
80
|
+
d: 11,
|
|
81
|
+
};
|
|
82
|
+
for (const key in dict) {
|
|
83
|
+
if (str.toLowerCase().startsWith(key))
|
|
84
|
+
return dict[key];
|
|
85
|
+
}
|
|
86
|
+
throw new Error('Invalid month name: ' + str);
|
|
87
|
+
}
|
|
88
|
+
// Convert two-digit year to a four digit year
|
|
89
|
+
export function yearToFull(year) {
|
|
90
|
+
if (typeof year === 'string')
|
|
91
|
+
year = parseInt(year.replace(/\D/g, ''));
|
|
92
|
+
if (year > 99)
|
|
93
|
+
return year;
|
|
94
|
+
// If year less than 20 years in the future, assume the 21st century
|
|
95
|
+
if (year < (new Date().getFullYear() + 20) % 100)
|
|
96
|
+
return year + 2000;
|
|
97
|
+
return year + 1900;
|
|
98
|
+
}
|
|
99
|
+
// Parses a string and returns the most plausible date
|
|
100
|
+
export function parseDate(value) {
|
|
101
|
+
if (value instanceof Date)
|
|
102
|
+
return value;
|
|
103
|
+
value = value.trim().toLowerCase();
|
|
104
|
+
let year = 0;
|
|
105
|
+
let month = 0;
|
|
106
|
+
let day = 0;
|
|
107
|
+
let hour = 0;
|
|
108
|
+
let minute = 0;
|
|
109
|
+
let second = 0;
|
|
110
|
+
const timeRE = new RegExp(/\d{1,2}\:\d\d(?:\:\d\ds?)?\s?(?:[a|p]m?)?/gi);
|
|
111
|
+
// If the value contains a time, set the time variables
|
|
112
|
+
if (timeRE.test(value)) {
|
|
113
|
+
const timeStr = value.match(timeRE)[0];
|
|
114
|
+
// Remove the time from the string
|
|
115
|
+
value = value.replace(timeStr, '').trim();
|
|
116
|
+
const timeParts = parseTime(timeStr);
|
|
117
|
+
// Assign the time to the variables
|
|
118
|
+
if (timeParts !== null)
|
|
119
|
+
({ hour, minute, second } = timeParts);
|
|
120
|
+
// If the value seems to be a time only, return a date with this time.
|
|
121
|
+
if (value.length <= 2) {
|
|
122
|
+
const now = new Date();
|
|
123
|
+
return new Date(now.getFullYear(), now.getMonth(), now.getDate(), hour, minute, second);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Strip day of the week from the string in English, French, or Spanish
|
|
127
|
+
const dayOfWeekRegex = /(^|\b)(mo|tu|we|th|fr|sa|su|lu|mard|mer|jeu|ve|dom)[\w]*\.?/gi;
|
|
128
|
+
value = value.replace(dayOfWeekRegex, '').trim();
|
|
129
|
+
// Convert now and today to the current date at midnight
|
|
130
|
+
const today = new Date(new Date().setHours(0, 0, 0, 0));
|
|
131
|
+
if (/(now|today)/.test(value))
|
|
132
|
+
return today;
|
|
133
|
+
if (value.includes('tomorrow'))
|
|
134
|
+
return new Date(today.setDate(today.getDate() + 1));
|
|
135
|
+
// Handle a undelimited 6 or 8-digit number and treat it as YYYYMMDD
|
|
136
|
+
if (value.length === 8)
|
|
137
|
+
value = value.replace(/(\d\d\d\d)(\d\d)(\d\d)/, '$1-$2-$3');
|
|
138
|
+
if (value.length === 6)
|
|
139
|
+
value = value.replace(/(\d\d)(\d\d)(\d\d)/, yearToFull(value.slice(0, 2)) + '-$2-$3');
|
|
140
|
+
try {
|
|
141
|
+
;
|
|
142
|
+
({ year, month, day } = guessDateParts(value));
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
return new Date('');
|
|
146
|
+
}
|
|
147
|
+
// Return date (month is 0-based)
|
|
148
|
+
return new Date(year, month - 1, day, hour, minute, second);
|
|
149
|
+
} // end parseDate
|
|
150
|
+
// Returns array of possible meanings for a token in a date string
|
|
151
|
+
// Pass an array of known parameters to exclude them from the guess
|
|
152
|
+
// Assumes months are 1-based.
|
|
153
|
+
export function guessDatePart(num, knownMeanings = [null, null, null]) {
|
|
154
|
+
const unknown = (arr) => arr.filter((i) => !knownMeanings.includes(i));
|
|
155
|
+
if (num === 0 || num > 31)
|
|
156
|
+
return unknown(['y']);
|
|
157
|
+
if (num > 12)
|
|
158
|
+
return unknown(['d', 'y']);
|
|
159
|
+
if (num >= 1 && num <= 12)
|
|
160
|
+
return unknown(['m', 'd', 'y']);
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
// Returns most likely meanings of each part of a date. Assumes 1-based months
|
|
164
|
+
export function guessDateParts(str) {
|
|
165
|
+
const tokens = str.split(/[\s-/:.,]+/).filter((i) => i !== '');
|
|
166
|
+
// If two tokens, add a year to the beginning
|
|
167
|
+
if (tokens.length < 3) {
|
|
168
|
+
// If one of the tokens is a 4-digit number, don't have enough info to guess the date
|
|
169
|
+
if (str.match(/\d{4}/) !== null)
|
|
170
|
+
throw new Error('Invalid Date');
|
|
171
|
+
else
|
|
172
|
+
tokens.unshift(String(new Date().getFullYear()));
|
|
173
|
+
}
|
|
174
|
+
const date = { year: 0, month: 0, day: 0 };
|
|
175
|
+
function assignPart(part, num) {
|
|
176
|
+
if (part === 'year')
|
|
177
|
+
date.year = yearToFull(num);
|
|
178
|
+
else
|
|
179
|
+
date[part] = num;
|
|
180
|
+
}
|
|
181
|
+
let count = 0;
|
|
182
|
+
while (!(date.year && date.month && date.day)) {
|
|
183
|
+
tokenLoop: for (const token of tokens) {
|
|
184
|
+
count++;
|
|
185
|
+
// If word
|
|
186
|
+
if (/^[a-zA-Zé]+$/.test(token)) {
|
|
187
|
+
if (!date.month)
|
|
188
|
+
assignPart('month', monthToNumber(token) + 1);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
// If the token is a year
|
|
192
|
+
if (/^'\d\d$/.test(token) || /^\d{3,5}$/.test(token)) {
|
|
193
|
+
if (!date.year)
|
|
194
|
+
assignPart('year', parseInt(token.replace(/'/, '')));
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
// If the token is a number
|
|
198
|
+
const num = parseInt(token);
|
|
199
|
+
if (isNaN(num)) {
|
|
200
|
+
console.error(`not date because ${token} isNaN`);
|
|
201
|
+
throw new Error('Invalid Date');
|
|
202
|
+
}
|
|
203
|
+
const meanings = guessDatePart(num, [
|
|
204
|
+
date.year ? 'y' : null,
|
|
205
|
+
date.month ? 'm' : null,
|
|
206
|
+
date.day ? 'd' : null,
|
|
207
|
+
]);
|
|
208
|
+
if (meanings.length == 1) {
|
|
209
|
+
if (meanings[0] === 'm' && !date.month) {
|
|
210
|
+
assignPart('month', num);
|
|
211
|
+
continue tokenLoop;
|
|
212
|
+
}
|
|
213
|
+
if (meanings[0] === 'd' && !date.day) {
|
|
214
|
+
assignPart('day', num);
|
|
215
|
+
continue tokenLoop;
|
|
216
|
+
}
|
|
217
|
+
if (meanings[0] === 'y' && !date.year) {
|
|
218
|
+
assignPart('year', num);
|
|
219
|
+
continue tokenLoop;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// If we have no idea what the token is after going through all of them
|
|
223
|
+
// set token to the first thing it could be starting with month
|
|
224
|
+
if (count > 3) {
|
|
225
|
+
if (!date.month && meanings.includes('m'))
|
|
226
|
+
assignPart('month', num);
|
|
227
|
+
else if (!date.day && meanings.includes('d'))
|
|
228
|
+
assignPart('day', num);
|
|
229
|
+
// If the previous two lines ran, the year will be assigned on the next iteration
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Should never take more than six iterations to figure out a valid date
|
|
233
|
+
if (count > 6)
|
|
234
|
+
throw new Error('Invalid Date');
|
|
235
|
+
}
|
|
236
|
+
if (date.year && date.month && date.day)
|
|
237
|
+
return date;
|
|
238
|
+
/* c8 ignore next */
|
|
239
|
+
throw new Error('Invalid Date');
|
|
240
|
+
}
|
|
241
|
+
// A simplified version of apps-date.ts's parseTime function.
|
|
242
|
+
export function parseTime(value) {
|
|
243
|
+
// if "now" or "today" is in the string, return the current time
|
|
244
|
+
value = value.trim().toLowerCase();
|
|
245
|
+
if (value === 'now') {
|
|
246
|
+
const now = new Date();
|
|
247
|
+
return { hour: now.getHours(), minute: now.getMinutes(), second: now.getSeconds() };
|
|
248
|
+
}
|
|
249
|
+
// If there is a 3-4 digit number, assume it's a time and add a colon
|
|
250
|
+
const timeParts = value.match(/(\d{3,4})/);
|
|
251
|
+
if (timeParts) {
|
|
252
|
+
const length = timeParts[1].length;
|
|
253
|
+
const hour = timeParts[1].slice(0, length == 3 ? 1 : 2);
|
|
254
|
+
const minutes = timeParts[1].slice(-2);
|
|
255
|
+
value = value.replace(timeParts[1], hour + ':' + minutes);
|
|
256
|
+
}
|
|
257
|
+
// Match a simple time without minutes or seconds and optional am/pm
|
|
258
|
+
const shortTimeRegex = new RegExp(/^(\d{1,2})(?::(\d{1,2}))?\s*(?:(a|p)m?)?$/i);
|
|
259
|
+
if (shortTimeRegex.test(value)) {
|
|
260
|
+
const shortParts = value.match(shortTimeRegex);
|
|
261
|
+
/* c8 ignore next */
|
|
262
|
+
if (shortParts === null)
|
|
263
|
+
return null;
|
|
264
|
+
value = shortParts[1] + ':' + (shortParts[2] || '00') + (shortParts[3] || '');
|
|
265
|
+
}
|
|
266
|
+
// Regex to match time in 0:0 format with optional seconds and am/pm
|
|
267
|
+
const timeRegex = new RegExp(/^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?\s*(?:(a|p)m?)?$/i);
|
|
268
|
+
if (!timeRegex.test(value))
|
|
269
|
+
return null;
|
|
270
|
+
const parts = value.match(timeRegex);
|
|
271
|
+
/* c8 ignore next */
|
|
272
|
+
if (parts === null)
|
|
273
|
+
return null;
|
|
274
|
+
const hour = parseInt(parts[1]);
|
|
275
|
+
const minute = parseInt(parts[2]);
|
|
276
|
+
const second = parts[3] ? parseInt(parts[3]) : 0;
|
|
277
|
+
const ampm = parts[4];
|
|
278
|
+
/* c8 ignore next */
|
|
279
|
+
if (isNaN(hour) || isNaN(minute) || isNaN(second))
|
|
280
|
+
return null;
|
|
281
|
+
if (ampm === 'p' && hour < 12)
|
|
282
|
+
return { hour: hour + 12, minute, second };
|
|
283
|
+
if (ampm === 'a' && hour === 12)
|
|
284
|
+
return { hour: 0, minute, second };
|
|
285
|
+
/* c8 ignore next 3 */
|
|
286
|
+
if (hour < 0 || hour > 23)
|
|
287
|
+
return null;
|
|
288
|
+
if (minute < 0 || minute > 59)
|
|
289
|
+
return null;
|
|
290
|
+
if (second < 0 || second > 59)
|
|
291
|
+
return null;
|
|
292
|
+
return { hour, minute, second };
|
|
293
|
+
} // parseTime
|
|
294
|
+
export function parseTimeToString(value, format = 'h:mm A') {
|
|
295
|
+
const time = parseTime(value);
|
|
296
|
+
if (time) {
|
|
297
|
+
const date = new Date();
|
|
298
|
+
date.setHours(time.hour);
|
|
299
|
+
date.setMinutes(time.minute);
|
|
300
|
+
date.setSeconds(time.second);
|
|
301
|
+
date.setMilliseconds(0);
|
|
302
|
+
return formatDateTime(date, format);
|
|
303
|
+
}
|
|
304
|
+
return '';
|
|
305
|
+
}
|
|
306
|
+
// Accepts a date or date-like string and returns a formatted date string
|
|
307
|
+
// Uses moment-compatible format strings
|
|
308
|
+
export function formatDateTime(date, format = 'YYYY-MM-DD') {
|
|
309
|
+
// Ensure the date is a valid date object
|
|
310
|
+
date = parseDate(date);
|
|
311
|
+
// if date is an invalid date object, return empty string
|
|
312
|
+
if (isNaN(date.getTime()))
|
|
313
|
+
return '';
|
|
314
|
+
const d = {
|
|
315
|
+
y: date.getFullYear(),
|
|
316
|
+
M: date.getMonth(),
|
|
317
|
+
D: date.getDate(),
|
|
318
|
+
W: date.getDay(),
|
|
319
|
+
H: date.getHours(),
|
|
320
|
+
m: date.getMinutes(),
|
|
321
|
+
s: date.getSeconds(),
|
|
322
|
+
ms: date.getMilliseconds(),
|
|
323
|
+
};
|
|
324
|
+
const pad = (n, w = 2) => (n + '').padStart(w, '0');
|
|
325
|
+
const getH = () => d.H % 12 || 12;
|
|
326
|
+
const getMeridiem = (hour) => (hour < 12 ? 'AM' : 'PM');
|
|
327
|
+
const monthToString = (month) => 'January|February|March|April|May|June|July|August|September|October|November|December'.split('|')[month];
|
|
328
|
+
function dayToString(day, len = 0) {
|
|
329
|
+
const days = 'Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday'.split('|');
|
|
330
|
+
return len ? days[day].slice(0, len) : days[day];
|
|
331
|
+
}
|
|
332
|
+
const matches = {
|
|
333
|
+
YY: String(d.y).slice(-2),
|
|
334
|
+
YYYY: d.y,
|
|
335
|
+
M: d.M + 1,
|
|
336
|
+
MM: pad(d.M + 1),
|
|
337
|
+
MMMM: monthToString(d.M),
|
|
338
|
+
MMM: monthToString(d.M).slice(0, 3),
|
|
339
|
+
D: String(d.D),
|
|
340
|
+
DD: pad(d.D),
|
|
341
|
+
d: String(d.W),
|
|
342
|
+
dd: dayToString(d.W, 2),
|
|
343
|
+
ddd: dayToString(d.W, 3),
|
|
344
|
+
dddd: dayToString(d.W),
|
|
345
|
+
H: String(d.H),
|
|
346
|
+
HH: pad(d.H),
|
|
347
|
+
h: getH(),
|
|
348
|
+
hh: pad(getH()),
|
|
349
|
+
A: getMeridiem(d.H),
|
|
350
|
+
a: getMeridiem(d.H).toLowerCase(),
|
|
351
|
+
m: String(d.m),
|
|
352
|
+
mm: pad(d.m),
|
|
353
|
+
s: String(d.s),
|
|
354
|
+
ss: pad(d.s),
|
|
355
|
+
SSS: pad(d.ms, 3),
|
|
356
|
+
};
|
|
357
|
+
return format.replace(/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g, (match, $1) => $1 || matches[match]);
|
|
358
|
+
} // end formatDateTime
|
|
359
|
+
export function parseDateToString(value, format) {
|
|
360
|
+
const date = parseDate(value);
|
|
361
|
+
if (isNaN(date.getTime()))
|
|
362
|
+
return '';
|
|
363
|
+
// if the format is undefined or has length of 0, set it to the default format
|
|
364
|
+
if (!format || format.length === 0)
|
|
365
|
+
format = 'YYYY-MMM-DD';
|
|
366
|
+
return formatDateTime(date, format);
|
|
367
|
+
}
|
|
368
|
+
// Check if a the value of a specified input is a valid date and is in a specified date range
|
|
369
|
+
export function isDate(value) {
|
|
370
|
+
if (typeof value !== 'string' && !(value instanceof Date))
|
|
371
|
+
return false;
|
|
372
|
+
let date = parseDate(value);
|
|
373
|
+
if (date === null || date === undefined)
|
|
374
|
+
return false;
|
|
375
|
+
return !isNaN(date.getTime());
|
|
376
|
+
}
|
|
377
|
+
// Check if a date is within the specified range
|
|
378
|
+
export function isDateInRange(date, range) {
|
|
379
|
+
if (range === 'past' && date > new Date())
|
|
380
|
+
return false;
|
|
381
|
+
if (range === 'future' && date.getTime() < new Date().setHours(0, 0, 0, 0))
|
|
382
|
+
return false;
|
|
383
|
+
// In the future, may add support for ranges like 'last 30 days' or 'next 3 months'
|
|
384
|
+
// or specific dates like '2019-01-01 to 2019-12-31'
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
export function isTime(value) {
|
|
388
|
+
let timeObj = parseTime(value);
|
|
389
|
+
if (timeObj === null)
|
|
390
|
+
return false;
|
|
391
|
+
return !isNaN(timeObj.hour) && !isNaN(timeObj.minute) && !isNaN(timeObj.second);
|
|
392
|
+
}
|
|
393
|
+
// Input validation for email fields
|
|
394
|
+
export function isEmail(value) {
|
|
395
|
+
// Emails cannot be longer than 255 characters
|
|
396
|
+
if (value.length > 255)
|
|
397
|
+
return false;
|
|
398
|
+
// This is a relatively simple regex just to check that an email has a valid TLD
|
|
399
|
+
// It will not catch all invalid emails, the next regex does that
|
|
400
|
+
let emailTLDRegex = new RegExp(/^.+@.+\.[a-zA-Z0-9]{2,}$/);
|
|
401
|
+
if (!emailTLDRegex.test(value))
|
|
402
|
+
return false;
|
|
403
|
+
// A comprehensive regular expression to check for valid emails. Does not allow for unicode characters.
|
|
404
|
+
let re = '';
|
|
405
|
+
// Begin local part. Allow alphanumeric characters and some special characters
|
|
406
|
+
re += "^([a-zA-Z0-9!#$%'*+/=?^_`{|}~-]+";
|
|
407
|
+
// Allow dot separated sequences of the above characters (representing multiple labels in the local part)
|
|
408
|
+
re += "(?:\\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*";
|
|
409
|
+
// Allow a quoted string (using either single or double quotes)
|
|
410
|
+
re += '|';
|
|
411
|
+
re +=
|
|
412
|
+
'"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*"';
|
|
413
|
+
// End of local part and begin domain part of email address
|
|
414
|
+
re += ')@(';
|
|
415
|
+
// Domain part can be either a sequence of labels, separated by dots, ending with a TLD
|
|
416
|
+
re += '(';
|
|
417
|
+
re += '(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\\.)+';
|
|
418
|
+
// TLD must be at least 2 characters long
|
|
419
|
+
re += '[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?';
|
|
420
|
+
re += ')';
|
|
421
|
+
// Or, it can be an address within square brackets but
|
|
422
|
+
// we will not allow this - real people shouldn't be using IP addresses in email addresses
|
|
423
|
+
re += ')$'; // End of string
|
|
424
|
+
let emailRegex = new RegExp(re);
|
|
425
|
+
return emailRegex.test(value);
|
|
426
|
+
}
|
|
427
|
+
// Parse a North American Numbering Plan phone number (xxx-xxx-xxxx)
|
|
428
|
+
export function parseNANPTel(value) {
|
|
429
|
+
// first character regex, strip anything that isn't part of the area code
|
|
430
|
+
value = value.replace(/^[^2-90]+/g, '');
|
|
431
|
+
// now the first number should be for the area code
|
|
432
|
+
value = value.replace(/(\d\d\d).*?(\d\d\d).*?(\d\d\d\d)(.*)/, '$1-$2-$3$4');
|
|
433
|
+
return value;
|
|
434
|
+
}
|
|
435
|
+
// Checks that the phone number is valid in North American Numbering Plan
|
|
436
|
+
export function isNANPTel(value) {
|
|
437
|
+
return /^\d\d\d-\d\d\d-\d\d\d\d$/.test(value);
|
|
438
|
+
}
|
|
439
|
+
export function parseInteger(value) {
|
|
440
|
+
return value.replace(/[^0-9]/g, '');
|
|
441
|
+
}
|
|
442
|
+
export function isNumber(value) {
|
|
443
|
+
return /^\-?\d*\.?\d*$/.test(value);
|
|
444
|
+
}
|
|
445
|
+
export function parseNumber(value) {
|
|
446
|
+
return value
|
|
447
|
+
.replace(/[^\-0-9.]/g, '') // all but digits, hyphens, and periods
|
|
448
|
+
.replace(/(^-)|(-)/g, (_match, p1) => (p1 ? '-' : '')) // all but the first hyphen
|
|
449
|
+
.replace(/(\..*)\./g, '$1'); // periods after a first one
|
|
450
|
+
}
|
|
451
|
+
export function isInteger(value) {
|
|
452
|
+
return /^\-?\d*$/.test(value);
|
|
453
|
+
}
|
|
454
|
+
// If the string isn't already a valid url, prepends 'https://'
|
|
455
|
+
export function parseUrl(value) {
|
|
456
|
+
value = value.trim();
|
|
457
|
+
const urlRegex = new RegExp('^(?:[a-z+]+:)?//', 'i');
|
|
458
|
+
if (urlRegex.test(value))
|
|
459
|
+
return value;
|
|
460
|
+
else
|
|
461
|
+
return 'https://' + value;
|
|
462
|
+
}
|
|
463
|
+
// Checks if this is a valid URL in a protocol agnostic way,
|
|
464
|
+
// allows for protocol-relative absolute URLs (eg //example.com)
|
|
465
|
+
export function isUrl(value) {
|
|
466
|
+
const urlRegex = new RegExp('^(?:[-a-z+]+:)?//', 'i');
|
|
467
|
+
return urlRegex.test(value);
|
|
468
|
+
}
|
|
469
|
+
export function parseZip(value) {
|
|
470
|
+
value = value
|
|
471
|
+
.replace(/[^0-9]/g, '')
|
|
472
|
+
.replace(/(.{5})(.*)/, '$1-$2')
|
|
473
|
+
.trim();
|
|
474
|
+
// If the zip code has 6 characters, remove a hyphen
|
|
475
|
+
if (value.length === 6)
|
|
476
|
+
value = value.replace(/-/, '');
|
|
477
|
+
return value;
|
|
478
|
+
}
|
|
479
|
+
export function isZip(value) {
|
|
480
|
+
const zipRegex = new RegExp(/^\d{5}(-\d{4})?$/);
|
|
481
|
+
return zipRegex.test(value);
|
|
482
|
+
}
|
|
483
|
+
export function parsePostalCA(value) {
|
|
484
|
+
value = value
|
|
485
|
+
.toUpperCase()
|
|
486
|
+
.replace(/[^A-Z0-9]/g, '')
|
|
487
|
+
.replace(/(.{3})\s*(.*)/, '$1 $2')
|
|
488
|
+
.trim();
|
|
489
|
+
return value;
|
|
490
|
+
}
|
|
491
|
+
export function isPostalCA(value) {
|
|
492
|
+
const postalRegex = new RegExp(/^[ABCEGHJKLMNPRSTVXY][0-9][ABCEGHJKLMNPRSTVWXYZ] ?[0-9][ABCEGHJKLMNPRSTVWXYZ][0-9]$/);
|
|
493
|
+
return postalRegex.test(value);
|
|
494
|
+
}
|
|
495
|
+
// Checks if the value is a valid CSS color
|
|
496
|
+
// Falls back to a regex if CSS.supports isn't available
|
|
497
|
+
export function isColor(value) {
|
|
498
|
+
if (['transparent', 'currentColor'].includes(value))
|
|
499
|
+
return true;
|
|
500
|
+
if (typeof value !== 'string' || !value.trim())
|
|
501
|
+
return false;
|
|
502
|
+
if (typeof CSS === 'object' && typeof CSS.supports === 'function') {
|
|
503
|
+
return CSS.supports('color', value);
|
|
504
|
+
}
|
|
505
|
+
// If CSS.supports isn't available, use regexes to check for valid rgb or hsl color values
|
|
506
|
+
// Not as comprehensive as the CSS.supports method, but should work in older browsers
|
|
507
|
+
return isColorRegex(value);
|
|
508
|
+
}
|
|
509
|
+
function isColorRegex(value) {
|
|
510
|
+
const rgbRegex = new RegExp(/^rgba?\(\s*(\d{1,3}%?,\s*){2}\d{1,3}%?\s*(?:,\s*(\.\d+|0+(\.\d+)?|1(\.0+)?|0|1\.0|\d{1,2}(\.\d*)?%|100%))?\s*\)$/);
|
|
511
|
+
const hslRegex = new RegExp(/^hsla?\(\s*\d+(deg|grad|rad|turn)?,\s*\d{1,3}%,\s*\s*\d{1,3}%(?:,\s*(\.\d+|0+(\.\d+)?|1(\.0+)?|0|1\.0|\d{1,2}(\.\d*)?%|100%))?\s*\)$/);
|
|
512
|
+
// Support for the newer space-separated syntax
|
|
513
|
+
const rgbSpaceRegex = new RegExp(/^rgba?\(\s*(\d{1,3}%?\s+){2}\d{1,3}%?\s*(?:\s*\/\s*(\.\d+|0+(\.\d+)?|1(\.0+)?|0|1\.0|\d{1,2}(\.\d*)?%|100%))?\s*\)$/);
|
|
514
|
+
const hslSpaceRegex = new RegExp(/^hsla?\(\s*\d+(deg|grad|rad|turn)?\s+\d{1,3}%\s+\s*\d{1,3}%(?:\s*\/\s*(\.\d+|0+(\.\d+)?|1(\.0+)?|0|1\.0|\d{1,2}(\.\d*)?%|100%))?\s*\)$/);
|
|
515
|
+
// Hex color regex (short and long formats with and without alpha)
|
|
516
|
+
const hexRegex = new RegExp(/^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i);
|
|
517
|
+
let colors = `aliceblue|antiquewhite|aqua|aquamarine|azure|beige|bisque|black|blanchedalmond|blue|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|cyan|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkgrey|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|firebrick|floralwhite|forestgreen|fuchsia|gainsboro|ghostwhite|gold|goldenrod|gray|green|greenyellow|grey|honeydew|hotpink|indianred|indigo|ivory|khaki|lavender|lavenderblush|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgray|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightslategrey|lightsteelblue|lightyellow|lime|limegreen|linen|magenta|maroon|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|navy|oldlace|olive|olivedrab|orange|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|purple|rebeccapurple|red|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|silver|skyblue|slateblue|slategray|slategrey|snow|springgreen|steelblue|tan|teal|thistle|tomato|turquoise|violet|wheat|white|whitesmoke|yellow|yellowgreen`;
|
|
518
|
+
const colorNameRegex = new RegExp(`^(${colors})$`, 'i');
|
|
519
|
+
return (rgbRegex.test(value) ||
|
|
520
|
+
hslRegex.test(value) ||
|
|
521
|
+
rgbSpaceRegex.test(value) ||
|
|
522
|
+
hslSpaceRegex.test(value) ||
|
|
523
|
+
hexRegex.test(value) ||
|
|
524
|
+
colorNameRegex.test(value));
|
|
525
|
+
}
|
|
526
|
+
// Used to convert color names to hex values
|
|
527
|
+
let colorCanvas = null;
|
|
528
|
+
// Cache of color names to hex values to reduce reads to canvas
|
|
529
|
+
const colorCache = new Map();
|
|
530
|
+
// Uses a Canvas element to convert a color name to a hex value
|
|
531
|
+
export function parseColor(value) {
|
|
532
|
+
value = value.trim().toLowerCase();
|
|
533
|
+
if (['transparent', 'currentcolor'].includes(value))
|
|
534
|
+
return value;
|
|
535
|
+
if (colorCache.has(value))
|
|
536
|
+
return colorCache.get(value);
|
|
537
|
+
if (colorCanvas === null) {
|
|
538
|
+
colorCanvas = document.createElement('canvas');
|
|
539
|
+
colorCanvas.willReadFrequently = true;
|
|
540
|
+
}
|
|
541
|
+
let ctx = colorCanvas.getContext('2d');
|
|
542
|
+
if (!ctx)
|
|
543
|
+
throw new Error("Can't get context from colorCanvas");
|
|
544
|
+
ctx.fillStyle = value;
|
|
545
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
546
|
+
let d = ctx.getImageData(0, 0, 1, 1).data;
|
|
547
|
+
let hex = '#' + ('000000' + ((d[0] << 16) | (d[1] << 8) | d[2]).toString(16)).slice(-6);
|
|
548
|
+
colorCache.set(value, hex);
|
|
549
|
+
return hex;
|
|
550
|
+
}
|
|
551
|
+
// Homogenizes the return of a custom validation function to a ValidationResult
|
|
552
|
+
// that has a boolean valid property and messages array of strings
|
|
553
|
+
export function normalizeValidationResult(res) {
|
|
554
|
+
let result = { valid: false, error: false, messages: [] };
|
|
555
|
+
if (typeof res === 'boolean')
|
|
556
|
+
return { valid: res, error: false, messages: [] };
|
|
557
|
+
if (typeof res === 'string')
|
|
558
|
+
return { valid: false, error: false, messages: [res] };
|
|
559
|
+
if (typeof res.valid === 'boolean')
|
|
560
|
+
result.valid = res.valid;
|
|
561
|
+
if (typeof res.message === 'string')
|
|
562
|
+
result.messages = [res.message];
|
|
563
|
+
if (typeof res.messages === 'string')
|
|
564
|
+
result.messages = [res.messages];
|
|
565
|
+
if (Array.isArray(res.messages))
|
|
566
|
+
result.messages = res.messages;
|
|
567
|
+
if (res.error === true)
|
|
568
|
+
result.error = true;
|
|
569
|
+
return result;
|
|
570
|
+
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jdlien/validator-utils",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"module": "dist/validator-utils.js",
|
|
6
|
+
"types": "dist/validator-utils.d.ts",
|
|
6
7
|
"files": [
|
|
7
8
|
"dist"
|
|
8
9
|
],
|
|
9
10
|
"description": "Validation and sanitization functions used by @jdlien/Validator.",
|
|
10
11
|
"scripts": {
|
|
11
12
|
"dev": "vite",
|
|
12
|
-
"build": "
|
|
13
|
+
"build": "vite build && tsc --declaration",
|
|
13
14
|
"preview": "vite preview",
|
|
14
15
|
"test": "vitest",
|
|
15
16
|
"coverage": "vitest --coverage"
|