commit a20e29de9ef2ba66c6cfb82f44d9b0f605338a50 Author: Hadi Khazaee Asl Date: Sun Aug 9 20:32:28 2026 +0330 Initial Commit on git.saherelm.ir ... diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..59d9a3a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7114b71 --- /dev/null +++ b/.gitignore @@ -0,0 +1,52 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +/dist +/tmp +/out-tsc +# Only exists if Bazel was run +/bazel-out + +# dependencies +/node_modules + +# profiling files +chrome-profiler-events*.json + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# misc +/.sass-cache +/connect.lock +/coverage +/libpeerconnection.log +npm-debug.log +yarn-error.log +testem.log +/typings + +# System Files +.DS_Store +Thumbs.db + +# +documentation +package-lock.json + +# +.angular \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8a5e4db --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# XFrameworkCore + +this is xFramework's core module and conatins core features of XFramework Library. + +## Maintainer + +Hadi Khazaee asl + +[https://www.saherelm.ir](https://www.saherelm.ir) + +[hadi_khazaee_asl@yahoo.com](mailto:hadi_khazaee_asl@yahoo.com) diff --git a/angular.json b/angular.json new file mode 100644 index 0000000..dff2fda --- /dev/null +++ b/angular.json @@ -0,0 +1,45 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "cli": { + "analytics": false + }, + "version": 1, + "newProjectRoot": "projects", + "projects": { + "x-framework-core": { + "projectType": "library", + "root": "projects/x-framework-core", + "sourceRoot": "projects/x-framework-core/src", + "prefix": "x", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:ng-packagr", + "options": { + "project": "projects/x-framework-core/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "projects/x-framework-core/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "projects/x-framework-core/tsconfig.lib.json" + }, + "ivy": { + "tsConfig": "projects/x-framework-core/tsconfig.lib.ivy.json" + } + }, + "defaultConfiguration": "production" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "projects/x-framework-core/src/test.ts", + "tsConfig": "projects/x-framework-core/tsconfig.spec.json", + "karmaConfig": "projects/x-framework-core/karma.conf.js" + } + } + } + } + }, + "defaultProject": "x-framework-core" +} diff --git a/jalali-moment.d.ts b/jalali-moment.d.ts new file mode 100644 index 0000000..f0618a3 --- /dev/null +++ b/jalali-moment.d.ts @@ -0,0 +1,758 @@ +declare function moment(inp?: moment.MomentInput, format?: moment.MomentFormatSpecification, strict?: boolean): moment.Moment; +declare function moment(inp?: moment.MomentInput, format?: moment.MomentFormatSpecification, language?: string, strict?: boolean): moment.Moment; + +declare namespace moment { + type RelativeTimeKey = 's' | 'm' | 'mm' | 'h' | 'hh' | 'd' | 'dd' | 'M' | 'MM' | 'y' | 'yy'; + type CalendarKey = 'sameDay' | 'nextDay' | 'lastDay' | 'nextWeek' | 'lastWeek' | 'sameElse' | string; + type LongDateFormatKey = 'LTS' | 'LT' | 'L' | 'LL' | 'LLL' | 'LLLL' | 'lts' | 'lt' | 'l' | 'll' | 'lll' | 'llll'; + + interface Locale { + [key: string]: any; + calendar(key?: CalendarKey, m?: Moment, now?: Moment): string; + + longDateFormat(key: LongDateFormatKey): string; + invalidDate(): string; + ordinal(n: number): string; + + preparse(inp: string): string; + postformat(inp: string): string; + relativeTime(n: number, withoutSuffix: boolean, + key: RelativeTimeKey, isFuture: boolean): string; + pastFuture(diff: number, absRelTime: string): string; + set(config: Object): void; + + months(): string[]; + jMonths(): string[]; + + months(m: Moment, format?: string): string; + jMonths(m: Moment): string; + monthsShort(): string[]; + jMonthsShort():string[]; + monthsShort(m: Moment, format?: string): string; + jMonthsParse(monthName: string): number; + monthsParse(monthName: string, format: string, strict: boolean): number; + monthsRegex(strict: boolean): RegExp; + monthsShortRegex(strict: boolean): RegExp; + + week(m: Moment): number; + firstDayOfYear(): number; + firstDayOfWeek(): number; + + weekdays(): string[]; + weekdays(m: Moment, format?: string): string; + weekdaysMin(): string[]; + weekdaysMin(m: Moment): string; + weekdaysShort(): string[]; + weekdaysShort(m: Moment): string; + weekdaysParse(weekdayName: string, format: string, strict: boolean): number; + weekdaysRegex(strict: boolean): RegExp; + weekdaysShortRegex(strict: boolean): RegExp; + weekdaysMinRegex(strict: boolean): RegExp; + + isPM(input: string): boolean; + meridiem(hour: number, minute: number, isLower: boolean): string; + } + + interface StandaloneFormatSpec { + format: string[]; + standalone: string[]; + isFormat?: RegExp; + } + + interface WeekSpec { + dow: number; + doy: number; + } + + type CalendarSpecVal = string | ((m?: MomentInput, now?: Moment) => string); + interface CalendarSpec { + sameDay?: CalendarSpecVal; + nextDay?: CalendarSpecVal; + lastDay?: CalendarSpecVal; + nextWeek?: CalendarSpecVal; + lastWeek?: CalendarSpecVal; + sameElse?: CalendarSpecVal; + + // any additional properties might be used with moment.calendarFormat + [x: string]: CalendarSpecVal | void; // undefined + } + + type RelativeTimeSpecVal = ( + string | + ((n: number, withoutSuffix: boolean, + key: RelativeTimeKey, isFuture: boolean) => string) + ); + type RelativeTimeFuturePastVal = string | ((relTime: string) => string); + + interface RelativeTimeSpec { + future: RelativeTimeFuturePastVal; + past: RelativeTimeFuturePastVal; + s: RelativeTimeSpecVal; + m: RelativeTimeSpecVal; + mm: RelativeTimeSpecVal; + h: RelativeTimeSpecVal; + hh: RelativeTimeSpecVal; + d: RelativeTimeSpecVal; + dd: RelativeTimeSpecVal; + M: RelativeTimeSpecVal; + MM: RelativeTimeSpecVal; + y: RelativeTimeSpecVal; + yy: RelativeTimeSpecVal; + } + + interface LongDateFormatSpec { + LTS: string; + LT: string; + L: string; + LL: string; + LLL: string; + LLLL: string; + + // lets forget for a sec that any upper/lower permutation will also work + lts?: string; + lt?: string; + l?: string; + ll?: string; + lll?: string; + llll?: string; + } + + type MonthWeekdayFn = (momentToFormat: Moment, format?: string) => string; + type WeekdaySimpleFn = (momentToFormat: Moment) => string; + + interface LocaleSpecification { + useGregorianParser?: boolean; + months?: string[] | StandaloneFormatSpec | MonthWeekdayFn; + monthsShort?: string[] | StandaloneFormatSpec | MonthWeekdayFn; + + weekdays?: string[] | StandaloneFormatSpec | MonthWeekdayFn; + weekdaysShort?: string[] | StandaloneFormatSpec | WeekdaySimpleFn; + weekdaysMin?: string[] | StandaloneFormatSpec | WeekdaySimpleFn; + + meridiemParse?: RegExp; + meridiem?: (hour: number, minute: number, isLower: boolean) => string; + + isPM?: (input: string) => boolean; + + longDateFormat?: LongDateFormatSpec; + calendar?: CalendarSpec; + relativeTime?: RelativeTimeSpec; + invalidDate?: string; + ordinal?: (n: number) => string; + ordinalParse?: RegExp; + + week?: WeekSpec; + + // Allow anything: in general any property that is passed as locale spec is + // put in the locale object so it can be used by locale functions + [x: string]: any; + } + + interface MomentObjectOutput { + years: number; + /* One digit */ + months: number; + /* Day of the month */ + date: number; + hours: number; + minutes: number; + seconds: number; + milliseconds: number; + } + + interface Duration { + humanize(withSuffix?: boolean): string; + + abs(): Duration; + + as(units: unitOfTime.Base): number; + get(units: unitOfTime.Base): number; + + milliseconds(): number; + asMilliseconds(): number; + + seconds(): number; + asSeconds(): number; + + minutes(): number; + asMinutes(): number; + + hours(): number; + asHours(): number; + + days(): number; + asDays(): number; + + weeks(): number; + asWeeks(): number; + + months(): number; + asMonths(): number; + + years(): number; + asYears(): number; + + add(inp?: DurationInputArg1, unit?: DurationInputArg2): Duration; + subtract(inp?: DurationInputArg1, unit?: DurationInputArg2): Duration; + + locale(): string; + locale(locale: LocaleSpecifier): Duration; + localeData(): Locale; + + toISOString(): string; + toJSON(): string; + + /** + * @deprecated since version 2.8.0 + */ + lang(locale: LocaleSpecifier): Moment; + /** + * @deprecated since version 2.8.0 + */ + lang(): Locale; + /** + * @deprecated + */ + toIsoString(): string; + } + + interface MomentRelativeTime { + future: any; + past: any; + s: any; + m: any; + mm: any; + h: any; + hh: any; + d: any; + dd: any; + M: any; + MM: any; + y: any; + yy: any; + } + + interface MomentLongDateFormat { + L: string; + LL: string; + LLL: string; + LLLL: string; + LT: string; + LTS: string; + + l?: string; + ll?: string; + lll?: string; + llll?: string; + lt?: string; + lts?: string; + } + + interface MomentParsingFlags { + empty: boolean; + unusedTokens: string[]; + unusedInput: string[]; + overflow: number; + charsLeftOver: number; + nullInput: boolean; + invalidMonth: string | void; // null + invalidFormat: boolean; + userInvalidated: boolean; + iso: boolean; + parsedDateParts: any[]; + meridiem: string | void; // null + } + + interface MomentParsingFlagsOpt { + empty?: boolean; + unusedTokens?: string[]; + unusedInput?: string[]; + overflow?: number; + charsLeftOver?: number; + nullInput?: boolean; + invalidMonth?: string; + invalidFormat?: boolean; + userInvalidated?: boolean; + iso?: boolean; + parsedDateParts?: any[]; + meridiem?: string; + } + + interface MomentBuiltinFormat { + __momentBuiltinFormatBrand: any; + } + + type MomentFormatSpecification = string | MomentBuiltinFormat | (string | MomentBuiltinFormat)[]; + + namespace unitOfTime { + type Base = ( + "year" | "years" | "y" | "jYear" | + "month" | "months" | "M" | "jMonth" | "jmonth" | + "week" | "weeks" | "w" | "jw" | + "day" | "days" | "d" | "jDay" | "jD" | + "hour" | "hours" | "h" | + "minute" | "minutes" | "m" | + "second" | "seconds" | "s" | + "millisecond" | "milliseconds" | "ms" + ); + + type _quarter = "quarter" | "quarters" | "Q"; + type _isoWeek = "isoWeek" | "isoWeeks" | "W"; + type _date = "date" | "dates" | "D"; + type DurationConstructor = Base | _quarter; + + type DurationAs = Base; + + type StartOf = Base | _quarter | _isoWeek | _date; + + type Diff = Base | _quarter; + + type MomentConstructor = Base | _date; + + type All = Base | _quarter | _isoWeek | _date | + "weekYear" | "weekYears" | "gg" | + "isoWeekYear" | "isoWeekYears" | "GG" | + "dayOfYear" | "dayOfYears" | "DDD" | + "weekday" | "weekdays" | "e" | + "isoWeekday" | "isoWeekdays" | "E"; + } + + interface MomentInputObject { + years?: number; + year?: number; + y?: number; + + months?: number; + month?: number; + M?: number; + + days?: number; + day?: number; + d?: number; + + dates?: number; + date?: number; + D?: number; + + hours?: number; + hour?: number; + h?: number; + + minutes?: number; + minute?: number; + m?: number; + + seconds?: number; + second?: number; + s?: number; + + milliseconds?: number; + millisecond?: number; + ms?: number; + } + + interface DurationInputObject extends MomentInputObject { + quarters?: number; + quarter?: number; + Q?: number; + + weeks?: number; + week?: number; + w?: number; + } + + interface MomentSetObject extends MomentInputObject { + weekYears?: number; + weekYear?: number; + gg?: number; + + isoWeekYears?: number; + isoWeekYear?: number; + GG?: number; + + quarters?: number; + quarter?: number; + Q?: number; + + weeks?: number; + week?: number; + w?: number; + + isoWeeks?: number; + isoWeek?: number; + W?: number; + + dayOfYears?: number; + dayOfYear?: number; + DDD?: number; + + weekdays?: number; + weekday?: number; + e?: number; + + isoWeekdays?: number; + isoWeekday?: number; + E?: number; + } + + interface FromTo { + from: MomentInput; + to: MomentInput; + } + + type MomentInput = Moment | Date | string | number | (number | string)[] | MomentInputObject | void; // null | undefined + type DurationInputArg1 = Duration | number | string | FromTo | DurationInputObject | void; // null | undefined + type DurationInputArg2 = unitOfTime.DurationConstructor; + type LocaleSpecifier = string | Moment | Duration | string[] | boolean; + + interface MomentCreationData { + input: MomentInput; + format?: MomentFormatSpecification; + locale: Locale; + isUTC: boolean; + strict?: boolean; + } + + interface Moment extends Object{ + format(format?: string): string; + + startOf(unitOfTime: unitOfTime.StartOf): Moment; + endOf(unitOfTime: unitOfTime.StartOf): Moment; + + add(amount?: DurationInputArg1, unit?: DurationInputArg2): Moment; + /** + * @deprecated reverse syntax + */ + add(unit: unitOfTime.DurationConstructor, amount: number|string): Moment; + + subtract(amount?: DurationInputArg1, unit?: DurationInputArg2): Moment; + /** + * @deprecated reverse syntax + */ + subtract(unit: unitOfTime.DurationConstructor, amount: number|string): Moment; + + calendar(time?: MomentInput, formats?: CalendarSpec): string; + + clone(): Moment; + + /** + * @return Unix timestamp in milliseconds + */ + valueOf(): number; + + // current date/time in local mode + local(keepLocalTime?: boolean): Moment; + isLocal(): boolean; + + // current date/time in UTC mode + utc(keepLocalTime?: boolean): Moment; + isUTC(): boolean; + /** + * @deprecated use isUTC + */ + isUtc(): boolean; + + parseZone(): Moment; + isValid(): boolean; + invalidAt(): number; + + hasAlignedHourOffset(other?: MomentInput): boolean; + + creationData(): MomentCreationData; + parsingFlags(): MomentParsingFlags; + + year(y: number): Moment; + year(): number; + jYear(y: number): Moment; + jYear():number; + /** + * @deprecated use year(y) + */ + years(y: number): Moment; + /** + * @deprecated use year() + */ + years(): number; + quarter(): number; + quarter(q: number): Moment; + quarters(): number; + quarters(q: number): Moment; + month(M: number|string): Moment; + jMonth(M: number|string): Moment; + month(): number; + jMonth(): number; + /** + * @deprecated use month(M) + */ + months(M: number|string): Moment; + /** + * @deprecated use month() + */ + months(): number; + day(d: number|string): Moment; + day(): number; + jDay(d: number|string): Moment; + jDay(): number; + days(d: number|string): Moment; + days(): number; + date(d: number): Moment; + jDate(d: number): Moment; + date(): number; + jDate(): number; + /** + * @deprecated use date(d) + */ + dates(d: number): Moment; + /** + * @deprecated use jDate(d) + */ + jDates(d: number): Moment; + /** + * @deprecated use date() + */ + dates(): number; + /** + * @deprecated use jDate() + */ + jDates():number; + + hour(h: number): Moment; + hour(): number; + hours(h: number): Moment; + hours(): number; + minute(m: number): Moment; + minute(): number; + minutes(m: number): Moment; + minutes(): number; + second(s: number): Moment; + second(): number; + seconds(s: number): Moment; + seconds(): number; + millisecond(ms: number): Moment; + millisecond(): number; + milliseconds(ms: number): Moment; + milliseconds(): number; + weekday(): number; + //TODO:jweekday + weekday(d: number): Moment; + isoWeekday(): number; + isoWeekday(d: number|string): Moment; + weekYear(): number; + jWeekYear(): number; + weekYear(d: number): Moment; + jWeekYear(d: number): Moment; + isoWeekYear(): number; + isoWeekYear(d: number): Moment; + week(): number; + jWeek(): number; + week(d: number): Moment; + jWeek(d: number): Moment; + weeks(): number; + jWeeks(): number;//TODO:check it + weeks(d: number): Moment; + isoWeek(): number; + isoWeek(d: number): Moment; + isoWeeks(): number; + isoWeeks(d: number): Moment; + weeksInYear(): number; + //TODO : jWeeksInYear + isoWeeksInYear(): number; + dayOfYear(): number; + jDayOfYear(): number; + dayOfYear(d: number): Moment; + jDayOfYear(d: number): Moment; + + from(inp: MomentInput, suffix?: boolean): string; + to(inp: MomentInput, suffix?: boolean): string; + fromNow(withoutSuffix?: boolean): string; + toNow(withoutPrefix?: boolean): string; + + diff(b: MomentInput, unitOfTime?: unitOfTime.Diff, precise?: boolean): number; + + toArray(): number[]; + toDate(): Date; + toISOString(): string; + inspect(): string; + toJSON(): string; + unix(): number; + + isLeapYear(): boolean; + jIsLeapYear(): boolean; + /** + * @deprecated in favor of utcOffset + */ + zone(): number; + zone(b: number|string): Moment; + utcOffset(): number; + utcOffset(b: number|string, keepLocalTime?: boolean): Moment; + isUtcOffset(): boolean; + daysInMonth(): number; + jDaysInMonth(): number; + isDST(): boolean; + + zoneAbbr(): string; + zoneName(): string; + + isBefore(inp?: MomentInput, granularity?: unitOfTime.StartOf): boolean; + isAfter(inp?: MomentInput, granularity?: unitOfTime.StartOf): boolean; + isSame(inp?: MomentInput, granularity?: unitOfTime.StartOf): boolean; + isSameOrAfter(inp?: MomentInput, granularity?: unitOfTime.StartOf): boolean; + isSameOrBefore(inp?: MomentInput, granularity?: unitOfTime.StartOf): boolean; + isBetween(a: MomentInput, b: MomentInput, granularity?: unitOfTime.StartOf, inclusivity?: "()" | "[)" | "(]" | "[]"): boolean; + + doAsGregorian(): Moment; + + /** + * @deprecated as of 2.8.0, use locale + */ + lang(language: LocaleSpecifier): Moment; + /** + * @deprecated as of 2.8.0, use locale + */ + lang(): Locale; + + locale(): string; + locale(locale: LocaleSpecifier): Moment; + + localeData(): Locale; + + /** + * @deprecated no reliable implementation + */ + isDSTShifted(): boolean; + + // NOTE(constructor): Same as moment constructor + /** + * @deprecated as of 2.7.0, use moment.min/max + */ + max(inp?: MomentInput, format?: MomentFormatSpecification, strict?: boolean): Moment; + /** + * @deprecated as of 2.7.0, use moment.min/max + */ + max(inp?: MomentInput, format?: MomentFormatSpecification, language?: string, strict?: boolean): Moment; + + // NOTE(constructor): Same as moment constructor + /** + * @deprecated as of 2.7.0, use moment.min/max + */ + min(inp?: MomentInput, format?: MomentFormatSpecification, strict?: boolean): Moment; + /** + * @deprecated as of 2.7.0, use moment.min/max + */ + min(inp?: MomentInput, format?: MomentFormatSpecification, language?: string, strict?: boolean): Moment; + + get(unit: unitOfTime.All): number; + set(unit: unitOfTime.All, value: number): Moment; + set(objectLiteral: MomentSetObject): Moment; + + toObject(): MomentObjectOutput; + } + + export var version: string; + export var fn: Moment; + + // NOTE(constructor): Same as moment constructor + export function utc(inp?: MomentInput, format?: MomentFormatSpecification, strict?: boolean): Moment; + export function utc(inp?: MomentInput, format?: MomentFormatSpecification, language?: string, strict?: boolean): Moment; + + export function unix(timestamp: number): Moment; + + export function invalid(flags?: MomentParsingFlagsOpt): Moment; + export function isMoment(m: any): m is Moment; + export function isDate(m: any): m is Date; + export function isDuration(d: any): d is Duration; + + /** + * @deprecated in 2.8.0 + */ + export function lang(language?: string): string; + /** + * @deprecated in 2.8.0 + */ + export function lang(language?: string, definition?: Locale): string; + + export function locale(language?: string): string; + export function locale(language?: string[]): string; + export function locale(language?: string, definition?: LocaleSpecification | void): string; // null | undefined + + export function localeData(key?: string | string[]): Locale; + + export function duration(inp?: DurationInputArg1, unit?: DurationInputArg2): Duration; + + // NOTE(constructor): Same as moment constructor + export function parseZone(inp?: MomentInput, format?: MomentFormatSpecification, strict?: boolean): Moment; + export function parseZone(inp?: MomentInput, format?: MomentFormatSpecification, language?: string, strict?: boolean): Moment; + + export function months(): string[]; + export function months(index: number): string; + export function months(format: string): string[]; + export function months(format: string, index: number): string; + export function monthsShort(): string[]; + export function monthsShort(index: number): string; + export function monthsShort(format: string): string[]; + export function monthsShort(format: string, index: number): string; + + export function jWeekOfYear(mom: Moment, firstDayOfWeek: number, firstDayOfWeekOfYear: number): MomentInput; + export function jDaysInMonth(jMear: number, jMonth: number): number; + export function jIsLeapYear(jYear: number): boolean; + export function useJalaliSystemPrimarily(): void; + export function useJalaliSystemSecondary(): void; + export var jConvert: any; + + export function from (date: string, locale: string, format?: string): Moment; + + export function weekdays(): string[]; + export function weekdays(index: number): string; + export function weekdays(format: string): string[]; + export function weekdays(format: string, index: number): string; + export function weekdays(localeSorted: boolean): string[]; + export function weekdays(localeSorted: boolean, index: number): string; + export function weekdays(localeSorted: boolean, format: string): string[]; + export function weekdays(localeSorted: boolean, format: string, index: number): string; + export function weekdaysShort(): string[]; + export function weekdaysShort(index: number): string; + export function weekdaysShort(format: string): string[]; + export function weekdaysShort(format: string, index: number): string; + export function weekdaysShort(localeSorted: boolean): string[]; + export function weekdaysShort(localeSorted: boolean, index: number): string; + export function weekdaysShort(localeSorted: boolean, format: string): string[]; + export function weekdaysShort(localeSorted: boolean, format: string, index: number): string; + export function weekdaysMin(): string[]; + export function weekdaysMin(index: number): string; + export function weekdaysMin(format: string): string[]; + export function weekdaysMin(format: string, index: number): string; + export function weekdaysMin(localeSorted: boolean): string[]; + export function weekdaysMin(localeSorted: boolean, index: number): string; + export function weekdaysMin(localeSorted: boolean, format: string): string[]; + export function weekdaysMin(localeSorted: boolean, format: string, index: number): string; + + export function min(...moments: MomentInput[]): Moment; + export function max(...moments: MomentInput[]): Moment; + + /** + * Returns unix time in milliseconds. Overwrite for profit. + */ + export function now(): number; + + export function defineLocale(language: string, localeSpec: LocaleSpecification | void): Locale; // null + export function updateLocale(language: string, localeSpec: LocaleSpecification | void): Locale; // null + + export function locales(): string[]; + + export function normalizeUnits(unit: unitOfTime.All): string; + export function relativeTimeThreshold(threshold: string): number | boolean; + export function relativeTimeThreshold(threshold: string, limit: number): boolean; + export function relativeTimeRounding(fn: (num: number) => number): boolean; + export function relativeTimeRounding(): (num: number) => number; + export function calendarFormat(m: Moment, now: Moment): string; + + /** + * Constant used to enable explicit ISO_8601 format parsing. + */ + export var ISO_8601: MomentBuiltinFormat; + + export var defaultFormat: string; + export var defaultFormatUtc: string; +} + +export = moment; diff --git a/package.json b/package.json new file mode 100644 index 0000000..d34d74d --- /dev/null +++ b/package.json @@ -0,0 +1,68 @@ +{ + "name": "x-framework-core-holder", + "version": "0.0.1", + "description": "an isolation layoer of xFrameworkCore module", + "keywords": [ + "x-framework", + "x-framework-core" + ], + "homepage": "https://saherelm.ir", + "author": { + "name": "Hadi Khazaee Asl", + "email": "hadi_khazaee_asl@yahoo.com", + "url": "https://saherelm.ir" + }, + "scripts": { + "ng": "ng", + "publishModule": "npm run buildModule && npm run packModule", + "build:ivy": "ng build x-framework-core --configuration=ivy", + "serveDocs": "compodoc -p projects/x-framework-core/tsconfig.lib.json --theme material -s", + "buildModule": "npm run preBuildModule && ng build x-framework-core --configuration production", + "packModule": "cd dist/x-framework-core && npm pack && shx mv *.tgz ../../../../pack && cd ../..", + "preBuildModule": "shx true --silent mkdir ../../pack && shx true --silent rm -rf dist && shx true --silent rm ../../pack/x-framework-core*.tgz", + "generateDocs": "compodoc -p projects/x-framework-core/tsconfig.lib.json --theme material --hideGenerator --disableSourceCode --disableDomTree --disableTemplateTab --disableStyleTab --disablePrivate --disableLifeCycleHooks --disableRoutesGraph --disableCoverage --customFavicon ../xFrameworkDocuments/Resources/favicon.png --customLogo ../xFrameworkDocuments/Resources/favicon.png" + }, + "private": true, + "dependencies": { + "@angular/animations": "~12.0.4", + "@angular/common": "~12.0.4", + "@angular/compiler": "~12.0.4", + "@angular/core": "~12.0.4", + "@angular/forms": "~12.0.4", + "@angular/platform-browser": "~12.0.4", + "@angular/platform-browser-dynamic": "~12.0.4", + "@angular/router": "~12.0.4", + "rxjs": "~6.6.0", + "tslib": "^2.1.0", + "zone.js": "~0.11.4" + }, + "devDependencies": { + "@angular-devkit/build-angular": "~12.0.4", + "@angular/cdk": "^12.1.2", + "@angular/cli": "~12.0.4", + "@angular/compiler-cli": "~12.0.4", + "@angular/material": "^12.1.2", + "@angular/material-moment-adapter": "^12.1.2", + "@compodoc/compodoc": "^1.1.14", + "@ionic/angular": "^5.6.11", + "@types/jasmine": "~3.6.0", + "@types/node": "^12.11.1", + "@types/uuid": "^8.3.1", + "ionicons": "^5.5.2", + "jalali-moment": "^3.3.10", + "jasmine-core": "~3.7.0", + "karma": "~6.3.0", + "karma-chrome-launcher": "~3.1.0", + "karma-coverage": "~2.0.3", + "karma-jasmine": "~4.0.0", + "karma-jasmine-html-reporter": "^1.5.0", + "libphonenumber-js": "^1.9.22", + "md5-typescript": "^1.0.5", + "moment": "^2.29.1", + "ng-packagr": "^12.0.0", + "shx": "^0.3.3", + "typescript": "~4.2.3", + "undefined": "^0.1.0", + "uuid": "^8.3.2" + } +} diff --git a/projects/x-framework-core/README.md b/projects/x-framework-core/README.md new file mode 100644 index 0000000..b0b61d0 --- /dev/null +++ b/projects/x-framework-core/README.md @@ -0,0 +1,11 @@ +# XFrameworkCore + +this is xFramework's core module and conatins core features of XFramework Library. + +## Maintainer + +Hadi Khazaee asl + + + +hadi_khazaee_asl@yahoo.com diff --git a/projects/x-framework-core/karma.conf.js b/projects/x-framework-core/karma.conf.js new file mode 100644 index 0000000..91eb715 --- /dev/null +++ b/projects/x-framework-core/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, '../../coverage/x-framework-core'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/projects/x-framework-core/ng-package.json b/projects/x-framework-core/ng-package.json new file mode 100644 index 0000000..fa14c7f --- /dev/null +++ b/projects/x-framework-core/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../dist/x-framework-core", + "lib": { + "entryFile": "src/public-api.ts" + } +} \ No newline at end of file diff --git a/projects/x-framework-core/package.json b/projects/x-framework-core/package.json new file mode 100644 index 0000000..06c9158 --- /dev/null +++ b/projects/x-framework-core/package.json @@ -0,0 +1,22 @@ +{ + "name": "x-framework-core", + "version": "0.0.1", + "description": "core features of xFramework", + "keywords": [ + "x-framework", + "x-framework-core" + ], + "homepage": "https://saherelm.ir", + "author": { + "name": "Hadi Khazaee Asl", + "email": "hadi_khazaee_asl@yahoo.com", + "url": "https://saherelm.ir" + }, + "peerDependencies": { + "@angular/common": "^12.0.4", + "@angular/core": "^12.0.4" + }, + "dependencies": { + "tslib": "^2.1.0" + } +} diff --git a/projects/x-framework-core/src/lib/classes/x-base.service.ts b/projects/x-framework-core/src/lib/classes/x-base.service.ts new file mode 100644 index 0000000..94d3922 --- /dev/null +++ b/projects/x-framework-core/src/lib/classes/x-base.service.ts @@ -0,0 +1,4 @@ +import { XLoggable } from './x-loggable'; + +export abstract class XBaseService extends XLoggable { +} diff --git a/projects/x-framework-core/src/lib/classes/x-hammer-gesture-config.ts b/projects/x-framework-core/src/lib/classes/x-hammer-gesture-config.ts new file mode 100644 index 0000000..eceb3cb --- /dev/null +++ b/projects/x-framework-core/src/lib/classes/x-hammer-gesture-config.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@angular/core'; +import { HammerGestureConfig } from '@angular/platform-browser'; + +@Injectable() +export class XHammerGestureConfig extends HammerGestureConfig { + public overrides = { + pinch: { + enable: false + }, + rotate: { + enable: false + }, + swipe: { + direction: 6 + } + } +} diff --git a/projects/x-framework-core/src/lib/classes/x-loggable.ts b/projects/x-framework-core/src/lib/classes/x-loggable.ts new file mode 100644 index 0000000..146ab9c --- /dev/null +++ b/projects/x-framework-core/src/lib/classes/x-loggable.ts @@ -0,0 +1,120 @@ +import { + isNullOrUndefined, + isNullOrEmptyString +} from '../tools/x-type-detect.tools'; +import { Inject } from '@angular/core'; +import { X_FRAMEWORK_CORE_CONFIG } from '../tokens/x-injectable-tokens'; +import { XFrameworkCoreConfig } from '../config/x-framework-core.config'; + +export enum XLogType { + Warn, + Error, + Debug, + Info +} + +export interface GetLogTag { + getLogTag(): string; +} + +export abstract class XLoggable implements GetLogTag { + // + // Constructor ... + constructor( + @Inject(X_FRAMEWORK_CORE_CONFIG) public config: XFrameworkCoreConfig + ) {} + + // + // GetLogTag Implementation ... + /** + * retrieve logging tag ... + */ + getLogTag(): string { + // + if (this.constructor) { + return `${this.config.baseLogTag}_[${this.constructor.name}]`; + } + + // + return `${this.config.baseLogTag}_[NON-Constructor]`; + } + + protected log(type: XLogType, message?: any, ...args: any[]) { + // + if (!this.config.enableLogging) { + return; + } + + // + let logMsg = !isNullOrEmptyString(message) + ? this.getLogTag() + ' => ' + message + : this.getLogTag(); + + // + const logArgs = args; + args.push(new Date()); + + // + switch (type) { + case XLogType.Debug: + // + logMsg = 'D: ' + logMsg; + + // + isNullOrUndefined(logArgs) + ? // tslint:disable-next-line:no-console + console.debug(logMsg) + : // tslint:disable-next-line:no-console + console.debug(logMsg, logArgs); + break; + + case XLogType.Error: + // + logMsg = 'E: ' + logMsg; + + // + isNullOrUndefined(logArgs) + ? console.error(logMsg) + : console.error(logMsg, logArgs); + break; + + case XLogType.Warn: + // + logMsg = 'W: ' + logMsg; + + // + isNullOrUndefined(logArgs) + ? console.warn(logMsg) + : console.warn(logMsg, logArgs); + break; + + case XLogType.Info: + // + logMsg = 'I: ' + logMsg; + + // + isNullOrUndefined(logArgs) + ? // tslint:disable-next-line:no-console + console.info(logMsg) + : // tslint:disable-next-line:no-console + console.info(logMsg, logArgs); + break; + } + } + + protected logDebug(message?: any, ...args: any) { + this.log(XLogType.Debug, message, args); + } + + protected logError(message?: any, ...args: any) { + this.log(XLogType.Error, message, args); + } + + protected logWarn(message?: any, ...args: any) { + this.log(XLogType.Warn, message, args); + } + + protected logInfo(message?: any, ...args: any) { + this.log(XLogType.Info, message, args); + } +} diff --git a/projects/x-framework-core/src/lib/classes/x-persian-date-adapter.ts b/projects/x-framework-core/src/lib/classes/x-persian-date-adapter.ts new file mode 100644 index 0000000..539e909 --- /dev/null +++ b/projects/x-framework-core/src/lib/classes/x-persian-date-adapter.ts @@ -0,0 +1,247 @@ +import * as moment_ from 'jalali-moment'; +const moment = moment_; +import { + MomentDateAdapter, + MAT_MOMENT_DATE_FORMATS, +} from '@angular/material-moment-adapter'; +import { XParam } from '../constants/x-param.enum'; +import { DateAdapter } from '@angular/material/core'; +import { applyLocale, XLocale } from '../typings/x-translation.typings'; +import { toPersianNumbers } from '../tools/x-string.tools'; +import { localeData, Moment, isMoment, invalid } from 'jalali-moment'; +import { isString } from '../tools/x-type-detect.tools'; + +export const PERSIAN_DATE_FORMATS = { + parse: { + dateInput: 'jYYYY/jMM/jDD', + }, + display: { + dateInput: 'jYYYY/jMM/jDD', + monthYearLabel: 'jYYYY jMMMM', + dateA11yLabel: 'jYYYY/jMM/jDD', + monthYearA11yLabel: 'jYYYY jMMMM', + }, +}; + +export class XPersianDateAdapter extends DateAdapter { + // + private mLocale: XLocale = 'fa-IR'; + + constructor() { + super(); + super.setLocale(this.mLocale); + } + + getYear(date: Moment): number { + return this.clone(date).jYear(); + } + + getMonth(date: Moment): number { + return this.clone(date).jMonth(); + } + + getDate(date: Moment): number { + return this.clone(date).jDate(); + } + + getDayOfWeek(date: Moment): number { + return this.clone(date).day(); + } + + getMonthNames(style: 'long' | 'short' | 'narrow'): string[] { + // + const jLocalData = localeData(this.mLocale); + + // + switch (style) { + case 'long': + case 'short': + return jLocalData._jMonths.slice(0); + case 'narrow': + return jLocalData._jMonthsShort.slice(0); + } + } + + getDateNames(): string[] { + const valuesArray = Array(31); + for (let i = 0; i < 31; i++) { + valuesArray[i] = toPersianNumbers(String(i + 1)); + } + return valuesArray; + } + + getDayOfWeekNames(style: 'long' | 'short' | 'narrow'): string[] { + switch (style) { + case 'long': + return localeData(this.mLocale).weekdays().slice(0); + case 'short': + return localeData(this.mLocale).weekdaysShort().slice(0); + case 'narrow': + return ['ی', 'د', 'س', 'چ', 'پ', 'ج', 'ش']; + } + } + + getYearName(date: Moment): string { + return toPersianNumbers(this.clone(date).jYear().toString()); + } + + getFirstDayOfWeek(): number { + return localeData(this.mLocale).firstDayOfWeek(); + } + + getNumDaysInMonth(date: Moment): number { + return this.clone(date).jDaysInMonth(); + } + + clone(date: Moment | Date | string | any): Moment { + // + const isDateMoment = moment.isMoment(date); + if (!isDateMoment) { + date = moment(new Date(date)); + } + + // + let result: Moment; + + // + try { + result = date.clone().locale(this.mLocale); + } catch { + // + result = this.today().clone().locale(this.mLocale); + } + + // + return result; + } + + createDate(year: number, month: number, date: number): Moment { + if (month < 0 || month > 11) { + throw Error( + `Invalid month index "${month}". Month index has to be between 0 and 11.` + ); + } + if (date < 1) { + throw Error(`Invalid date "${date}". Date has to be greater than 0.`); + } + const result = moment() + .jYear(year) + .jMonth(month) + .jDate(date) + .hours(0) + .minutes(0) + .seconds(0) + .milliseconds(0) + .locale(this.mLocale); + + if (this.getMonth(result) !== month) { + throw Error(`Invalid date ${date} for month with index ${month}.`); + } + if (!result.isValid()) { + throw Error(`Invalid date "${date}" for month with index "${month}".`); + } + return result; + } + + today(): Moment { + return moment().locale(this.mLocale); + } + + parse(value: any, parseFormat: string | string[]): Moment | null { + if (value && typeof value === 'string') { + return moment(value, parseFormat, this.mLocale); + } + return value ? moment(value).locale(this.mLocale) : null; + } + + format(date: Moment | Date | string | any, displayFormat: string): string { + // + if (isString(date)) { + date = applyLocale(date.toString(), 'en-US'); + } + + // + date = this.clone(date); + + // + if (!this.isValid(date)) { + throw Error('JalaliMomentDateAdapter: Cannot format invalid date.'); + } + + // + try { + return applyLocale(date.format(displayFormat), this.mLocale); + } catch { + return applyLocale(this.today().format(displayFormat), this.mLocale); + } + } + + addCalendarYears(date: Moment, years: number): Moment { + return this.clone(date).add(years, 'jYear'); + } + + addCalendarMonths(date: Moment, months: number): Moment { + return this.clone(date).add(months, 'jmonth'); + } + + addCalendarDays(date: Moment, days: number): Moment { + return this.clone(date).add(days, 'jDay'); + } + + toIso8601(date: Moment): string { + return this.clone(date).format(); + } + + isDateInstance(obj: any): boolean { + return isMoment(obj); + } + + isValid(date: Moment): boolean { + return this.clone(date).isValid(); + } + + invalid(): Moment { + return invalid(); + } + + deserialize(value: any): Moment | null { + let date; + if (value instanceof Date) { + date = moment(value); + } + if (typeof value === 'string') { + if (!value) { + return null; + } + date = moment(value).locale(this.mLocale); + } + if (date && this.isValid(date)) { + return date; + } + return super.deserialize(value); + } +} + +export const DateAdapterFactory = (matDateLocale: XLocale) => { + // + if (matDateLocale === 'fa-IR') { + return new XPersianDateAdapter(); + } + + // + return new MomentDateAdapter(matDateLocale); +}; + +export const MatDateFormatFactory = (matDateLocale: XLocale) => { + // + if (matDateLocale === 'fa-IR') { + return PERSIAN_DATE_FORMATS; + } + + // + return MAT_MOMENT_DATE_FORMATS; +}; + +export const LocaleFactory = () => { + return window[XParam.Language]; +}; diff --git a/projects/x-framework-core/src/lib/classes/x-renderer.class.ts b/projects/x-framework-core/src/lib/classes/x-renderer.class.ts new file mode 100644 index 0000000..5aa4f7b --- /dev/null +++ b/projects/x-framework-core/src/lib/classes/x-renderer.class.ts @@ -0,0 +1,429 @@ +import { + NgZone, + Inject, + Renderer2, + ElementRef, + RendererStyleFlags2, +} from '@angular/core'; +import { Observable } from 'rxjs'; +import { + isFunction, + isNullOrUndefined, + isNullOrEmptyString, +} from '../tools/x-type-detect.tools'; +import { XLoggable } from './x-loggable'; +import { hasChild } from '../tools/x-array.tools'; +import { ViewportRuler } from '@angular/cdk/overlay'; +import { XRef, XStandardType } from '../typings/x.typings'; +import { XCssProperties } from '../typings/x-setting.typings'; +import { toObservable, toPromise } from '../tools/x-observable.tools'; +import { X_FRAMEWORK_CORE_CONFIG } from '../tokens/x-injectable-tokens'; +import { XFrameworkCoreConfig } from '../config/x-framework-core.config'; +import { waitForXElement } from '../tools/x-object.tools'; + +export class XRenderer extends XLoggable { + // + //#endregion Props ... + private PREFIX = '--'; + //#endregion + + // + //#region Constructor ... + constructor( + @Inject(X_FRAMEWORK_CORE_CONFIG) public config: XFrameworkCoreConfig, + public element: ElementRef, + public renderer: Renderer2, + public ruler: ViewportRuler, + public ngZone: NgZone + ) { + super(config); + } + //#endregion + + // + //#region Document ... + getElementById(id: string) { + return document.getElementById(id); + } + + getElementsByClassName(className: string) { + return document.getElementsByClassName(className); + } + + getElementsByName(elementName: string) { + return document.getElementsByName(elementName); + } + + getElementsByTagName( + tagName: K | string + ) { + return document.getElementsByTagName(tagName); + } + + getRootNode() { + return document.getRootNode(); + } + + waitForElement(selector: string) { + return waitForXElement(selector); + } + + querySelector(selector: K | string) { + return document.querySelector(selector); + } + + querySelectorAll( + selector: K | string + ) { + return document.querySelectorAll(selector); + } + //#endregion + + // + //#region Renderer ... + /** + * convert an element to HTML Element + * for accessing props ... + * + * @param el elementRef of the selected element + */ + getElement(el?: ElementRef | HTMLElement) { + // + if (!el) { + el = this.element; + } + + // + return el instanceof ElementRef + ? ((el as ElementRef).nativeElement as HTMLElement) + : (el as HTMLElement); + } + + /** + * adding classes to an element ... + * + * @param clz the classes names + * @param el the element + */ + addClass(clz: string, el?: ElementRef | HTMLElement) { + this.renderer.addClass(this.getElement(el), clz); + } + + /** + * remove classes from an element ... + * + * @param clz the classes names + * @param el the element + */ + removeClass(clz: string, el?: ElementRef | HTMLElement) { + this.renderer.removeClass(this.getElement(el), clz); + } + + /** + * create an element ... + * + * @param name create an element + * @param nameSpace adding a namespace + */ + createElement(name: string, nameSpace?: string) { + return this.renderer.createElement(name, nameSpace); + } + + /** + * create a Text Element ... + * + * @param value a text value + */ + createText(value: string) { + return this.renderer.createText(value); + } + + /** + * adding a child element to another element ... + * + * @param parent the parent element + * @param newChild the child element + */ + appendChild(parent: any, newChild: any) { + this.renderer.appendChild(parent, newChild); + } + + /** + * adding a child element to another element ... + * + * @param child child element + * @param el parent element + */ + appendChildToElement(child: any, el?: ElementRef | HTMLElement) { + this.renderer.appendChild(this.getElement(el), child); + } + + /** + * set an attribute of an element ... + * + * @param name attribute name + * @param value attribute value + * @param el destination element + * @param namespace name space name + */ + setAttribute( + name: string, + value: string, + el?: ElementRef | HTMLElement, + namespace?: string | null + ) { + this.renderer.setAttribute(this.getElement(el), name, value, namespace); + } + + /** + * remove an attribute from an element ... + * + * @param name attribute name + * @param el destination element + * @param namespace name space name + */ + removeAttribute( + name: string, + el?: ElementRef | HTMLElement, + namespace?: string | null + ) { + this.renderer.removeAttribute(this.getElement(el), name, namespace); + } + + /** + * set style of an element ... + * + * @param style the style name + * @param value style value + * @param el destination element + * @param flags flags + */ + setStyle( + style: string, + value: any, + el?: ElementRef | HTMLElement, + flags?: RendererStyleFlags2 + ) { + this.renderer.setStyle(this.getElement(el), style, value, flags); + } + + /** + * remove an style from an element + * + * @param style style name + * @param el destination element + * @param flags flags + */ + removeStyle( + style: string, + el?: ElementRef | HTMLElement, + flags?: RendererStyleFlags2 + ) { + this.renderer.removeStyle(this.getElement(el), style, flags); + } + + /** + * set property to an element ... + * + * @param name property name + * @param value property value + * @param el destination element + */ + setProperty(name: string, value: any, el?: ElementRef | HTMLElement) { + this.renderer.setProperty(this.getElement(el), name, value); + } + + /** + * set value of specific node + * + * @param node specific element + * @param value value + */ + setValue(node: any, value: string) { + this.renderer.setValue(node, value); + } + + /** + * recieve computed style of an element ... + * + * @param el destination element + */ + getComputedStyle(el?: ElementRef | HTMLElement) { + return getComputedStyle(this.getElement(el)); + } + + /** + * prepare a css var name + * + * @param key css variable name + */ + prepareCssKey(key: XCssProperties) { + return `${this.PREFIX}${key}`; + } + + /** + * remove css key prefix ... + * + * @param key a prepared css var ... + */ + removeCssKeyPrefix(key: XCssProperties) { + return key.replace(this.PREFIX, ''); + } + + /** + * read specific css var value of an element + * @param name css variable name + * @param element destination element + */ + getCssVar(name: XCssProperties, el?: ElementRef | HTMLElement) { + // + return this.getComputedStyle(el).getPropertyValue(this.prepareCssKey(name)); + } + + /** + * set css var into element ... + * + * @param name css var name + * @param value css var value + * @param elements destination element of array of elements + */ + setCssVar( + name: XCssProperties, + value: string, + el?: ElementRef | HTMLElement + ) { + // + if (isNullOrEmptyString(name)) { + return; + } + + // + this.getElement(el).style.setProperty(this.prepareCssKey(name), value); + } + + /** + * load script and add to component dynamically ... + * + * @param path path to js file + */ + addScript(path: string) { + // + const s = this.renderer.createElement('script'); + s.type = 'text/javascript'; + s.src = path; + s.text = ``; + this.renderer.appendChild(document.body, s); + } + + /** + * add script content ... + * + * @param script the script block + */ + addScriptContent(script: string) { + // + const s = this.renderer.createElement('script'); + s.type = 'text/javascript'; + s.text = `${script}`; + this.renderer.appendChild(document.body, s); + } + //#endregion + + // + //#region Providers ... + public getElementBoundaryRect(el?: ElementRef | HTMLElement) { + return this.getElement(el).getBoundingClientRect(); + } + + public getElementBoundaryWidth(el?: ElementRef | HTMLElement) { + return this.getElementBoundaryRect().width; + } + + public getElementBoundaryHeight(el?: ElementRef | HTMLElement) { + return this.getElementBoundaryRect().height; + } + + public getElementClientWidth(el?: ElementRef | HTMLElement) { + return this.getElement(el).clientWidth; + } + + public getElementClientHeight(el?: ElementRef | HTMLElement) { + return this.getElement(el).clientHeight; + } + + public getElementOffsetWidth(el?: ElementRef | HTMLElement) { + return this.getElement(el).offsetWidth; + } + + public getElementOffsetHeight(el?: ElementRef | HTMLElement) { + return this.getElement(el).offsetHeight; + } + //#endregion + + // + //#region Zone Actions ... + run(fn: (...args: any[]) => T, applyThis?: any, applyArgs?: any[]): T { + return this.ngZone.run(fn, applyThis, applyArgs); + } + + runTask( + fn: (...args: any[]) => T, + applyThis?: any, + applyArgs?: any[], + name?: string + ): T { + return this.ngZone.runTask(fn, applyThis, applyArgs, name); + } + + runGuarded( + fn: (...args: any[]) => T, + applyThis?: any, + applyArgs?: any[] + ): T { + return this.ngZone.runGuarded(fn, applyThis, applyArgs); + } + + runOutsideAngular(fn: (...args: any[]) => T): T { + return this.ngZone.runOutsideAngular(fn); + } + //#endregion + + // + //#region Action Runnings ... + async runTimedOut( + fn: (...args: any[]) => T, + delay?: XStandardType + ) { + // + let desiredDelay = 0; + if (!isNullOrUndefined(delay)) { + desiredDelay = await toPromise(toObservable(delay)); + } + + // + if (!fn) { + return null; + } + + // + return await new Promise((res, rej) => { + // + setTimeout(() => { + // + try { + // + const result = fn(); + res(result); + } catch { + // + rej(null); + } + }, desiredDelay); + }); + } + //#endregion + + // + //#region Private ... + //#endregion +} diff --git a/projects/x-framework-core/src/lib/config/x-framework-core.config.ts b/projects/x-framework-core/src/lib/config/x-framework-core.config.ts new file mode 100644 index 0000000..0e6c7a9 --- /dev/null +++ b/projects/x-framework-core/src/lib/config/x-framework-core.config.ts @@ -0,0 +1,66 @@ +import { XCountryCode } from '../typings/x.typings'; +import { XNotificationSound } from '../typings/x-notification.typings'; +import { XLanguageDescriptor, XLocale } from '../typings/x-translation.typings'; + +/** + * Application Configuration Structure ... + */ +export interface XFrameworkCoreConfig { + // + // Environment Identifier ... + isProd: boolean; + + // + // App Info ... + appInstanceName: string; + appNameResource: string; + appCompanyResource: string; + appVersion: string; + + // + poweredValue: string; + + // + // Logging Configuration ... + enableLogging: boolean; + baseLogTag: string; + + // + // Language Configuration ... + availableLanguages: XLanguageDescriptor[]; + defaultLanguage: XLocale; + + // + // Audio Service Configurations ... + defaultNotificationDelay: number; + notificationAudioSources: XNotificationSound[]; + + // + // Security Service Configurations ... + secretKey: string; + + // + // Image Related Configs ... + minAllowedImageSize: number; + maxAllowedImageSize: number; + allowedImageExtensions: string[]; + + // + // Profile Image Configs ... + maxProfileImageSize: number; + maxProfileImagesUploadFiles: number; + minAllowedImageCroppableSize: number; + + // + // DCountry Code ... + defaultCountryCode: XCountryCode; + + // + // Page Sizes ... + defaultPageSize: number; + pageSizes: number[]; + + // + // Action Delays ... + defaultActionDelay: number; +} diff --git a/projects/x-framework-core/src/lib/constants/x-border.enum.ts b/projects/x-framework-core/src/lib/constants/x-border.enum.ts new file mode 100644 index 0000000..dd047bd --- /dev/null +++ b/projects/x-framework-core/src/lib/constants/x-border.enum.ts @@ -0,0 +1,14 @@ +export enum XBorderStyle { + Dotted = 'dotted', + Dashed = 'dashed', + Solid = 'solid', + Double = 'double', + Grove = 'groove', + Ridge = 'ridge', + Inset = 'inset', + Outset = 'outset', + None = 'none', + Hidden = 'hidden', +} + +export type XBorderStyleIdentifier = XBorderStyle | string; diff --git a/projects/x-framework-core/src/lib/constants/x-crud.enum.ts b/projects/x-framework-core/src/lib/constants/x-crud.enum.ts new file mode 100644 index 0000000..2dc90f5 --- /dev/null +++ b/projects/x-framework-core/src/lib/constants/x-crud.enum.ts @@ -0,0 +1,7 @@ +export enum XCrudAction { + Add = 'add', + Update = 'update', + Delete = 'delete' +} + +export type XCrudActionIdentifier = XCrudAction | string; diff --git a/projects/x-framework-core/src/lib/constants/x-exception.resources.ts b/projects/x-framework-core/src/lib/constants/x-exception.resources.ts new file mode 100644 index 0000000..2fa64b7 --- /dev/null +++ b/projects/x-framework-core/src/lib/constants/x-exception.resources.ts @@ -0,0 +1,359 @@ +import { XExceptionIndex } from '../typings/x-exception.typings'; + +export enum XExceptionIDs { + // + //#region Invalid Exceptions 1xx ... + InvalidData = 100, + InvalidDate = 101, + BadRequest = 102, + InvalidArgs = 103, + InvalidMessage = 104, + InvalidToken = 105, + InvalidVerificationCode = 106, + InvalidEmailAddress = 107, + InvalidMobileNumber = 108, + InvalidPassword = 109, + InavlidUrl = 110, + InvalidDevice = 111, + InvalidProfileImage = 112, + InavlidFile = 113, + InvalidUserName = 114, + //#endregion + + // + //#region Not (...) Exceptions 2xx ... + NotFound = 200, + NotAllowed = 201, + NotAuthorized = 202, + NotVisible = 203, + //#endregion + + // + //#region User/Registration/Account/Bann Exceptions 3xx ... + AccountLockedOut = 300, + LoginFailed = 301, + UserRegisteredBefore = 302, + UserDisabled = 303, + DeviceBanned = 304, + VerificationCodeResendNotAllowed = 305, + PasswordsSame = 306, + MobilesSame = 307, + EmailsSame = 308, + MobileInUsed = 309, + EmailInUsed = 310, + MobileNotConfirmed = 311, + EmailNotConfirmed = 312, + PostBanned = 313, + UserBanned = 314, + //#endregion + + // + //#region General Exceptions 4xx ... + Unknown = 400, + Duplicate = 401, + ActionFailed = 402, + ExpiredDate = 403, + Unavailable = 404, + MaxAllowedTagExceeded = 405, + Timeout = 406, + //#endregion + + // + //#region File Exceptions 5xx ... + EmptyFile = 500, + MaxFileSizeExceeded = 501, + UnsupportedFileType = 502, + MaxAllowedMediaExceeded = 503, + //#endregion + + // + //#region Configuration Exceptions 6xx ... + InvalidConfiguration = 600, + StorageServiceInitialFailed = 601, + MessageServiceInitialFailed = 602, + //#endregion + + // + //#region xFramework Exceptions 9xx ... + MaxAllowedFilesExceeded = 900, + MinFilesSizeExceeded = 901, + MaxFilesSizeExceeded = 902, + MustAcceptTerms = 903, + UnsupportedBrowser = 904, + PermissionDenied = 905, + PositionUnavailable = 906, + StorageNotSupported = 907, + MustLogout = 908, + InvalidLocale = 909, + InvalidUserSelector = 910, + //#endregion +} + +/** + * a collection of Available Exceptions on System ... + */ +export const Exceptions: XExceptionIndex = { + // + //#region Invalid Exceptions 1xx ... + InvalidData: { + id: 100, + message: 'Invalid Data ...', + }, + InvalidDate: { + id: 101, + message: 'Invalid Date ...', + }, + BadRequest: { + id: 102, + message: 'Bad Request ...', + }, + InvalidArgs: { + id: 103, + message: 'Invalid Args ...', + }, + InvalidMessage: { + id: 104, + message: 'Invalid Message ...', + }, + InvalidToken: { + id: 105, + message: 'Invalid Token ...', + }, + InvalidVerificationCode: { + id: 106, + message: 'Invalid Verification Code ...', + }, + InvalidEmailAddress: { + id: 107, + message: 'Invalid Email Address ...', + }, + InvalidMobileNumber: { + id: 108, + message: 'Invalid Mobile Number ...', + }, + InvalidPassword: { + id: 109, + message: 'Invalid Password ...', + }, + InavlidUrl: { + id: 110, + message: 'Inavlid Url ...', + }, + InvalidDevice: { + id: 111, + message: 'Invalid Device ...', + }, + InvalidProfileImage: { + id: 112, + message: 'Invalid Profile Image ...', + }, + InavlidFile: { + id: 113, + message: 'Invalid File ...', + }, + InvalidUserName: { + id: 114, + message: 'Invalid Username ...', + }, + //#endregion + + // + //#region Not (...) Exceptions 2xx ... + NotFound: { + id: 200, + message: 'Not Found ...', + }, + NotAllowed: { + id: 201, + message: 'Not Allowed ...', + }, + NotAuthorized: { + id: 202, + message: 'Not Authorized ...', + }, + NotVisible: { + id: 203, + message: 'Not Visible ...', + }, + //#endregion + + // + //#region User/Registration/Account/Bann Exceptions 3xx ... + AccountLockedOut: { + id: 300, + message: 'Account Locked out Until: (%$s)', + }, + LoginFailed: { + id: 301, + message: 'Login Failed ...', + }, + UserRegisteredBefore: { + id: 302, + message: 'User Registered Before ...', + }, + UserDisabled: { + id: 303, + message: 'User Disabled ...', + }, + DeviceBanned: { + id: 304, + message: 'Device Banned Until:(%$s)', + }, + VerificationCodeResendNotAllowed: { + id: 305, + message: `You can't Request Verification code Until:(%$s)`, + }, + PasswordsSame: { + id: 306, + message: 'Passwords Same ...', + }, + MobilesSame: { + id: 307, + message: 'Mobiles Same ...', + }, + EmailsSame: { + id: 308, + message: 'Emails Same ...', + }, + MobileInUsed: { + id: 309, + message: 'Mobile In Used ...', + }, + EmailInUsed: { + id: 310, + message: 'Email In Used ...', + }, + MobileNotConfirmed: { + id: 311, + message: 'Mobile Not Confirmed ...', + }, + EmailNotConfirmed: { + id: 312, + message: 'Email Not Confirmed ...', + }, + PostBanned: { + id: 313, + message: 'Post Banned ...', + }, + UserBanned: { + id: 314, + message: 'User Banned ...', + }, + //#endregion + + // + //#region General Exceptions 4xx ... + Unknown: { + id: 400, + message: 'Unknown Error ...', + }, + Duplicate: { + id: 401, + message: 'Duplicate ...', + }, + ActionFailed: { + id: 402, + message: 'Action Failed ...', + }, + ExpiredDate: { + id: 403, + message: 'Expired Date ...', + }, + Unavailable: { + id: 404, + message: 'Unavailable ...', + }, + MaxAllowedTagExceeded: { + id: 405, + message: 'Max Allowed Tag Exceeded ...', + }, + Timeout: { + id: 406, + message: 'Timed out ...', + }, + //#endregion + + // + //#region File Exceptions 5xx ... + EmptyFile: { + id: 500, + message: 'Empty File ...', + }, + MaxFileSizeExceeded: { + id: 501, + message: 'max File size exceeded ...', + }, + UnsupportedFileType: { + id: 502, + message: 'Unsupported File Type ...', + }, + MaxAllowedMediaExceeded: { + id: 503, + message: 'Max Allowed Media Exceeded ...', + }, + //#endregion + + // + //#region Configuration Exceptions 6xx ... + InvalidConfiguration: { + id: 600, + message: 'Invalid Configuration ...', + }, + StorageServiceInitialFailed: { + id: 601, + message: 'Storage Service Initiallization Failed ...', + }, + MessageServiceInitialFailed: { + id: 602, + message: 'Message Service Initialization Failed ...', + }, + //#endregion + + // + //#region xFramework Exceptions 9xx ... + MaxAllowedFilesExceeded: { + id: 900, + message: 'Max Allowed Files Exceeded ...', + }, + MinFilesSizeExceeded: { + id: 901, + message: 'Min Files Size not Exceeded ...', + }, + MaxFilesSizeExceeded: { + id: 902, + message: 'Max Files Size Exceeded ...', + }, + MustAcceptTerms: { + id: 903, + message: 'Must Accept Terms And Conditions ...', + }, + UnsupportedBrowser: { + id: 904, + message: 'Browser does not support location services ...', + }, + PermissionDenied: { + id: 905, + message: 'You have rejected access to your location ...', + }, + PositionUnavailable: { + id: 906, + message: 'Unable to determine your location ...', + }, + StorageNotSupported: { + id: 907, + message: 'Service timeout has been reached ...', + }, + MustLogout: { + id: 908, + message: `logged in user's can't access to this content ...`, + }, + InvalidLocale: { + id: 909, + message: 'Invalid Locale ...', + }, + InvalidUserSelector: { + id: 910, + message: 'Invalid User Selector ...', + }, + //#endregion +}; diff --git a/projects/x-framework-core/src/lib/constants/x-file.enum.ts b/projects/x-framework-core/src/lib/constants/x-file.enum.ts new file mode 100644 index 0000000..5e65f5e --- /dev/null +++ b/projects/x-framework-core/src/lib/constants/x-file.enum.ts @@ -0,0 +1,588 @@ +// +//#region XFile Types ... +export enum XFileType { + Image, + ProfileImage, + CoverImage, + Audio, + Video, + Document, +} + +export enum XGeneralThumbnails { + UNKNOWN = 'unknown', + IMAGE = 'image', + AUDIO = 'audio', + VIDEO = 'video', + DOCUMENT = 'document', +} + +export const TypesMatcher = { + IMAGE: 'image', + VIDEO: 'video', + AUDIO: 'audio', +}; +//#endregion + +// +//#region File Extensions ... +export enum XAudioFileExtensions { + Aif = '.aif', + Cda = '.cda', + Iff = '.iff', + Mid = '.mid', + Midi = '.midi', + Mp3 = '.mp3', + Mpa = '.mpa', + Wav = '.wav', + Wma = '.wma', + Wpl = '.wpl', + Aa = '.aa', + Aac = '.aac', + Aax = '.aax', + Act = '.act', + Aiff = '.aiff', + Alac = '.alac', + Amr = '.amr', + Ape = '.ape', + Au = '.au', + Awb = '.awb', + Dss = '.dss', + Dvf = '.dvf', + Flac = '.flac', + Gsm = '.gsm', + Iklax = '.iklax', + Ivs = '.ivs', + M4a = '.m4a', + M4b = '.m4b', + M4p = '.m4p', + Mmf = '.mmf', + Mpc = '.mpc', + Msv = '.msv', + Nmf = '.nmf', + Ogg = '.ogg', + Oga = '.oga', + Mogg = '.mogg', + Opus = '.opus', + Ra = '.ra', + Rm = '.rm', + Raw = '.raw', + Rf64 = '.rf64', + Sln = '.sln', + Tta = '.tta', + Voc = '.voc', + Vox = '.vox', + Wv = '.wv', + Eightsvx = '.8svx', +} + +export enum XVideoFileExtensions { + Avi = '.avi', + Flv = '.flv', + H264 = '.h264', + M4v = '.m4v', + Mkv = '.mkv', + Mov = '.mov', + Mp4 = '.mp4', + Mpg = '.mpg', + Mpeg = '.mpeg', + Rm = '.rm', + Swf = '.swf', + Vob = '.vob', + Wmv = '.wmv', + ThreeG2 = '.3g2', + ThreeGp = '.3gp', + Webm = '.webm', + Viv = '.viv', + Gifv = '.gifv', + Svi = '.svi', + Roq = '.roq', + Rmvb = '.rmvb', + Yuv = '.yuv', + Qt = '.qt', + Ogv = '.ogv', + Ogg = '.ogg', + Nsv = '.nsv', + Mng = '.mng', + M4p = '.m4p', + M2v = '.m2v', + Mp2 = '.mp2', + Mpe = '.mpe', + Mpv = '.mpv', + Mts = '.mts', + M2ts = '.m2ts', + Mxf = '.mxf', + Gif = '.gif', + F4v = '.f4v', + F4p = '.f4p', + F4a = '.f4a', + F4b = '.f4b', + Drc = '.drc', + Amv = '.amv', + Asf = '.asf', +} + +export enum XImageFileExtensions { + Ai = '.ai', + Bmp = '.bmp', + Gif = '.gif', + Jpeg = '.jpeg', + Jpg = '.jpg', + Max = '.max', + Obj = '.obj', + Png = '.png', + Ps = '.ps', + Psd = '.psd', + Svg = '.svg', + Tif = '.tif', + Tiff = '.tiff', + Threeds = '.3ds', + Threedm = '.3dm', + Ico = '.ico', + Icns = '.icns', +} + +export enum XDocumentFileExtensions { + Asp = '.asp', + Aspx = '.aspx', + Cer = '.cer', + Cfm = '.cfm', + Cgi = '.cgi', + Pl = '.pl', + Cs = '.cs', + Css = '.css', + SCss = '.scss', + Htm = '.htm', + Html = '.html', + Js = '.js', + Jsp = '.jsp', + JSON = '.json', + MD = '.md', + VSCodeWorkspace = '.code-workspace', + Part = '.part', + Php = '.php', + Rss = '.rss', + Xhtml = '.xhtml', + Bak = '.bak', + Cfg = '.cfg', + Cpl = '.cpl', + Cur = '.cur', + Dll = '.dll', + Dmp = '.dmp', + Drv = '.drv', + Ini = '.ini', + Lnk = '.lnk', + Msi = '.msi', + Sys = '.sys', + Tmp = '.tmp', + Accdb = '.accdb', + Csv = '.csv', + Dat = '.dat', + Db = '.db', + Dbf = '.dbf', + Log = '.log', + Mdb = '.mdb', + Pdb = '.pdb', + Sav = '.sav', + Sql = '.sql', + Tar = '.tar', + TarGz = '.tar.gz', + Key = '.key', + Odp = '.odp', + Pps = '.pps', + Ppt = '.ppt', + Pptx = '.pptx', + Ods = '.ods', + Xlr = '.xlr', + Xls = '.xls', + Xlsx = '.xlsx', + Doc = '.doc', + Docx = '.docx', + Odt = '.odt', + Msg = '.msg', + Pdf = '.pdf', + Rtf = '.rtf', + Tex = '.tex', + Txt = '.txt', + Wks = '.wks', + Wps = '.wps', + Wpd = '.wpd', + Apk = '.apk', + Bat = '.bat', + Com = '.com', + Exe = '.exe', + Py = '.py', + Pyw = '.pyw', + Wsf = '.wsf', + Appx = '.appx', + SevenZ = '.7z', + A = '.a', + Aapkg = '.aapkg', + Ace = '.ace', + Alz = '.alz', + Bke = '.bke', + Arj = '.arj', + B = '.b', + Ba = '.ba', + Bin = '.bin', + Bz2 = '.bz2', + Xaml = '.xaml', + Deb = '.deb', + Dmg = '.dmg', + Dn = '.dn', + Egg = '.egg', + Egt = '.egt', + Ezip = '.ezip', + Ess = '.ess', + Gz = '.gz', + Ipg = '.ipg', + Jar = '.jar', + Lzh = '.lzh', + Lz = '.lz', + Lzo = '.lzo', + Lzma = '.lzma', + Mhtml = '.mhtml', + Nth = '.nth', + Oar = '.oar', + Pak = '.pak', + Par = '.par', + Par2 = '.par2', + Paf = '.paf', + Pea = '.pea', + Pyk = '.pyk', + Pk3 = '.pk3', + Pk4 = '.pk4', + Pxz = '.pxz', + Rar = '.rar', + Sis = '.sis', + Sisx = '.sisx', + Skb = '.skb', + Sq = '.sq', + Tib = '.tib', + Xap = '.xap', + Xz = '.xz', + Z = '.z', + Ts = '.ts', + Zoo = '.zoo', + Zip = '.zip', +} +//#endregion + +// +//#region File Extension Descriptions ... +export enum XAudioFileExtensionDescriptions { + Aif = 'AIF/Audio Interchange audio', + Cda = 'CD audio track', + Iff = 'Interchange File Format', + Mid = 'MIDI audio', + Midi = 'MIDI audio', + Mp3 = 'MPEG Layer III Audio', + Mpa = 'MPEG-2 audio', + Wav = 'Standard audio file container format', + Wma = 'Windows Media Audio format', + Wpl = 'Windows Media Player playlist', + Aa = 'A low-bitrate audiobook container format', + Aac = 'Advanced Audio Coding format', + Aax = 'An Audiobook format', + Act = 'lossy ADPCM 8 kbit/s compressed audio format', + Aiff = 'Apple, A standard uncompressed CD-quality, audio file format', + Alac = 'Apple, An audio coding format developed by Apple Inc', + Amr = 'AMR-NB audio, used primarily for speech', + Ape = "Monkey's Audio lossless audio compression format", + Au = 'Sun Microsystems standard audio file format', + Awb = 'AMR-WB audio, used primarily for speech', + Dss = 'Olympus proprietary format', + Dvf = 'A Sony proprietary format for compressed voice files', + Flac = 'Free Lossless Audio Codec', + Gsm = 'Designed for telephony use in Europe', + Iklax = 'iKlax Media proprietary format', + Ivs = 'Digital Rights Management developed by 3D Solar UK Ltd', + M4a = 'audio-only MPEG-4 file, used by Apple for unprotected music', + M4b = 'Audiobook / podcast extension with AAC or ALAC encoded audio in an MPEG-4 container', + M4p = 'A version of AAC with proprietary Digital Rights Management developed by Apple for use in music downloaded from their iTunes Music Store and their music streaming service known as Apple Music', + Mmf = 'Yamaha, Samsung A Samsung audio format', + Mpc = 'Musepack, an open source lossy audio codec', + Msv = 'Sony proprietary format for Memory Stick compressed voice', + Nmf = 'NICE Media Player audio', + Ogg = 'Xiph.Org Foundation A free, open source container format', + Oga = 'Xiph.Org Foundation A free, open source container format', + Mogg = 'Xiph.Org Foundation A free, open source container format', + Opus = 'lossy audio compression format developed by the Internet Engineering Task Force (IETF) and made especially suitable for interactive real-time applications over the Internet', + Ra = 'RealAudio format designed for streaming audio over the Internet', + Rm = 'RealAudio format designed for streaming audio over the Internet', + Raw = 'raw file can contain audio in any format', + Rf64 = 'One successor to the Wav format', + Sln = 'Signed Linear PCM format used by Asterisk', + Tta = 'The True Audio, real-time lossless audio codec', + Voc = 'Creative Technology The file format', + Vox = 'Adaptive Differential Pulse Code Modulation', + Wv = 'Format for wavpack', + Eightsvx = 'Electronic Arts The IFF-8SVX format for 8-bit sound samples', +} + +export enum XVideoFileExtensionDescriptions { + Avi = 'Audio Video Interleave', + Flv = 'Adobe Flash Video', + H264 = 'H.264 Video', + M4v = 'Apple MP4 Video, MPEG-4 Part 12', + Mkv = 'Matroska Multimedia Container', + Mov = 'Apple QuickTime Movie', + Mp4 = 'MPEG-4 Video', + Mpg = 'MPEG Video', + Mpeg = 'MPEG Video', + Rm = 'Real Media', + Swf = 'Shockwave flash', + Vob = 'DVD Video Object', + Wmv = 'Windows Media Video', + ThreeG2 = '3GPP2 Multimedia', + ThreeGp = '3GPP multimedia', + Webm = 'Royalty-free format created for HTML5 video', + Viv = 'VivoActive based upon H.263 video', + Gifv = 'Video alternative to GIF', + Svi = 'Samsung video format for portable players', + Roq = 'used by Quake', + Rmvb = 'RealMedia Variable Bitrate', + Yuv = 'Raw video format', + Qt = 'QuickTime File Format', + Ogv = 'Ogg Video', + Ogg = 'Ogg Video', + Nsv = 'Nullsoft Streaming Video', + Mng = 'Multiple-image Network Graphics', + M4p = 'MPEG-4 Part 14', + M2v = 'MPEG-2 Part 3', + Mp2 = 'MPEG-1 Part 2', + Mpe = 'MPEG-1', + Mpv = 'MPEG-1', + Mts = 'MPEG Transport Stream', + M2ts = 'MPEG Transport Stream', + Mxf = 'Material Exchange Format', + Gif = 'Simple animation, inefficient compression', + F4v = 'Adobe Flash Video', + F4p = 'Adobe Flash Video', + F4a = 'Adobe Flash Video', + F4b = 'Adobe Flash Video', + Drc = 'Dirac', + Amv = 'Modified version of AVI', + Asf = 'Advanced Systems Format', +} + +export enum XImageFileExtensionDescriptions { + Ai = 'Adobe Illustrator', + Bmp = 'Bitmap image', + Gif = 'Graphical Interchange Format', + Jpeg = 'Joints Photographic Experts Group', + Jpg = 'Joints Photographic Experts Group', + Max = '3ds Max Scene', + Obj = 'Wavefront 3D Object', + Png = 'Portable Network Graphic image', + Ps = 'PostScript', + Psd = 'Adobe Photoshop Document image', + Svg = 'Scalable Vector Graphics', + Tif = 'TIFF image', + Tiff = 'TIFF image', + Threeds = '3D Studio Scene', + Threedm = 'Rhino 3D Model', + Ico = 'Icon', + Icns = 'macOS X icon resource', +} + +export enum XDocumentFileExtensionDescriptions { + Asp = 'Active Server Page', + Aspx = 'Active Server Page', + Cer = 'Internet security certificate', + Cfm = 'ColdFusion Markup', + Cgi = 'Common Gateway Interface Script', + Pl = 'Perl script', + Cs = 'C# document', + Css = 'Cascading Style Sheet', + SCss = 'Sass and Less Style Sheet', + Htm = 'Hypertext Markup Language', + Html = 'Hypertext Markup Language', + Js = 'JavaScript', + Jsp = 'Java Server Page', + JSON = 'Java Script Object Notation', + MD = 'Markdown', + VSCodeWorkspace = 'VSCode Workspace', + Part = 'Partially downloaded', + Php = 'PHP Source Code', + Rss = 'Rich Site Summary', + Xhtml = 'Extensible Hypertext Markup Language', + Bak = 'Backup', + Cfg = 'Configuration', + Cpl = 'Windows Control panel', + Cur = 'Windows cursor', + Dll = 'Dynamic Link Library', + Dmp = 'Dump', + Drv = 'Device driver', + Ini = 'Initialization', + Lnk = 'Windows shortcut', + Msi = 'Windows installer package', + Sys = 'Windows system', + Tmp = 'Temporary', + Accdb = 'Access 2007 Database', + Csv = 'Comma separated value', + Dat = 'Data', + Db = 'Database', + Dbf = 'Database', + Log = 'Log', + Mdb = 'Microsoft Access database', + Pdb = 'Program Database', + Sav = 'Save', + Sql = 'Structured Query Language database', + Tar = 'Linux / Unix tarball file archive', + TarGz = 'gzipped tar', + Key = 'Keynote presentation', + Odp = 'OpenOffice Impress presentation', + Pps = 'PowerPoint slide show', + Ppt = 'PowerPoint presentation', + Pptx = 'PowerPoint Open XML presentation', + Ods = 'OpenOffice Calc spreadsheet', + Xlr = 'Microsoft Works spreadsheet', + Xls = 'Microsoft Excel', + Xlsx = 'Microsoft Excel Open XML spreadsheet', + Doc = 'Microsoft Word', + Docx = 'Microsoft Word Open XML', + Odt = 'OpenOffice Writer document', + Msg = 'Outlook Mail Message', + Pdf = 'Portable Document Format', + Rtf = 'Rich Text Format', + Tex = 'LaTeX document', + Txt = 'Plain text', + Wks = 'Lotus 1-2-3 Spreadsheet', + Wps = 'Microsoft Works Word Processor Document', + Wpd = 'WordPerfect document', + Apk = 'Android package', + Bat = 'Batch', + Bin = 'Binary', + Com = 'MS-DOS command', + Exe = 'Executable', + Jar = 'Java Archive', + Py = 'Python', + Pyw = 'Python', + Wsf = 'Windows Script', + Appx = 'Microsoft Application Package', + SevenZ = '7-Zip compressed', + A = 'An external file extension for C/C++', + Aapkg = 'ArchestrA IDE', + Ace = 'ACE compressed', + Alz = 'ALZip compressed', + Bke = 'BackupEarth.com data compression', + Arj = 'ARJ compressed', + B = 'Similar to .a, but less compressed', + Ba = 'Scifer External Archive Type', + Bz2 = 'bzip2', + Xaml = 'Used in programs like Visual Studio to create exe files', + Deb = 'Debian install package', + Dmg = 'Apple compressed/encrypted format', + Dn = 'Adobe Dimension CC file format', + Egg = 'Alzip Egg Edition compressed', + Egt = 'EGT Universal Document', + Ezip = 'EGT Compressed Folder', + Ess = 'SmartSense', + Gz = 'Compressed', + Ipg = 'Format in which Apple Inc. packages their iPod games', + Lzh = 'Lempel, Ziv, Huffman', + Lz = 'Lempel, Ziv', + Lzo = 'Lempel, Ziv', + Lzma = 'Lempel Ziv Markov chain algorithm compressed', + Mhtml = 'Mime HTML (Hyper-Text Markup Language) code', + Nth = 'Nokia Theme Used by Nokia Series 40 Cellphones', + Oar = 'OAR archive', + Pak = 'Enhanced type of .ARC archive', + Par = 'Parchive', + Par2 = 'Parchive', + Paf = 'Portable Application File', + Pea = 'PeaZip archive', + Pyk = 'Compressed', + Pk3 = 'Quake 3 archive', + Pk4 = 'Opens similarly to a zip archive.', + Pxz = 'A compressed layered image', + Rar = 'Rar Archive', + Sis = 'Symbian Application Package', + Sisx = 'Symbian Application Package', + Skb = 'Google SketchUp backup', + Sq = 'Squish Compressed Archive', + Tib = 'Acronis True Image backup', + Xap = 'Windows Phone Application Package', + Xz = 'xz compressed files, based on LZMA/LZMA2 algorithm', + Z = 'Unix compress', + Ts = 'Typescript', + Zoo = 'based on LZW', + Zip = 'popular compression format', +} +//#endregion + +export type XFileExtensions = + | XAudioFileExtensions + | XVideoFileExtensions + | XImageFileExtensions + | XDocumentFileExtensions; + +export type XFileExtensionDescriptions = + | XAudioFileExtensionDescriptions + | XVideoFileExtensionDescriptions + | XImageFileExtensionDescriptions + | XDocumentFileExtensionDescriptions; + +// +//#region Files Informations Constants ... +// +const XImageFileKeys = Object.keys(Object.assign({}, XImageFileExtensions)); +const XImageFilesInfo = XImageFileKeys.map((ik) => { + return { + identifier: ik, + type: XFileType.Image, + typeIdentifier: XGeneralThumbnails.IMAGE, + extension: XImageFileExtensions[ik], + description: XImageFileExtensionDescriptions[ik], + }; +}); + +// +const XAudioFileKeys = Object.keys(Object.assign({}, XAudioFileExtensions)); +const XAudioFilesInfo = XAudioFileKeys.map((ik) => { + return { + identifier: ik, + type: XFileType.Audio, + typeIdentifier: XGeneralThumbnails.AUDIO, + extension: XAudioFileExtensions[ik], + description: XAudioFileExtensionDescriptions[ik], + }; +}); + +// +const XVideoFileKeys = Object.keys(Object.assign({}, XVideoFileExtensions)); +const XVideoFilesInfo = XVideoFileKeys.map((ik) => { + return { + identifier: ik, + type: XFileType.Video, + typeIdentifier: XGeneralThumbnails.VIDEO, + extension: XVideoFileExtensions[ik], + description: XVideoFileExtensionDescriptions[ik], + }; +}); + +// +const XDocumentFileKeys = Object.keys( + Object.assign({}, XDocumentFileExtensions) +); +const XDocumentFilesInfo = XDocumentFileKeys.map((ik) => { + return { + identifier: ik, + type: XFileType.Document, + typeIdentifier: XGeneralThumbnails.DOCUMENT, + extension: XDocumentFileExtensions[ik], + description: XDocumentFileExtensionDescriptions[ik], + }; +}); + +/** + * provides all informations about supported files ... + */ +export const XFileInfos = [ + ...XImageFilesInfo, + ...XVideoFilesInfo, + ...XAudioFilesInfo, + ...XDocumentFilesInfo, +]; +//#endregion diff --git a/projects/x-framework-core/src/lib/constants/x-header.enum.ts b/projects/x-framework-core/src/lib/constants/x-header.enum.ts new file mode 100644 index 0000000..3782673 --- /dev/null +++ b/projects/x-framework-core/src/lib/constants/x-header.enum.ts @@ -0,0 +1,24 @@ +export enum XHeader { + File = 'file', + Files = 'files', + Scope = 'scope', + Accept = 'Accept', + ExpiresAt = 'expires_at', + XPoweredBy = 'X-PoweredBy', + ActionToken = 'actionToken', + ContentType = 'Content-Type', + RefreshToken = 'refresh_token', + Authorization = 'Authorization', + AuthorizationPrefix = 'Bearer ', + RevisionChecksum = 'revision_checksum', + RejectUnauthorized = 'rejectUnauthorized', +} + +export enum XContentType { + AppJson = 'application/json', + AppOctetStream = 'application/octet-stream', +} + +export function getAuthorizationHeaderValue(token: string) { + return `${XHeader.AuthorizationPrefix}${token}`; +} diff --git a/projects/x-framework-core/src/lib/constants/x-param.enum.ts b/projects/x-framework-core/src/lib/constants/x-param.enum.ts new file mode 100644 index 0000000..0fe3b3b --- /dev/null +++ b/projects/x-framework-core/src/lib/constants/x-param.enum.ts @@ -0,0 +1,42 @@ +export enum XParam { + // + // QueryParams and Headers ... + Id = 'id', + ReturnUrl = 'ru', + Type = 'type', + // + Errors = 'errors', + // + Item = 'item', + Items = 'items', + // + Option = 'option', + Options = 'options', + // + HasBack = 'b', + // + // Header + Language = 'language', + // + // XQuery interface Params ... + Filter = 'Filter', + ContainsDetail = 'ContainsDetail', + EnableTracking = 'EnableTracking', + SortBy = 'SortBy', + IsAscending = 'IsAscending', + Page = 'Page', + PageSize = 'PageSize', + // + // XPageRequest interface Params ... + First = 'First', + Last = 'Last', + After = 'After', + Before = 'Before', + DescendingSort = 'DescendingSort', + // + // Exception ... + Exception = 'ex', +} +//#endregion + +export type XParamIdentifier = XParam | string; diff --git a/projects/x-framework-core/src/lib/constants/x-setting.resources.ts b/projects/x-framework-core/src/lib/constants/x-setting.resources.ts new file mode 100644 index 0000000..a212f1f --- /dev/null +++ b/projects/x-framework-core/src/lib/constants/x-setting.resources.ts @@ -0,0 +1,3 @@ +export enum XSettingsStorageKey { + Locale = '[SETTINGS] Locale' +} diff --git a/projects/x-framework-core/src/lib/constants/x-translation.resources.ts b/projects/x-framework-core/src/lib/constants/x-translation.resources.ts new file mode 100644 index 0000000..78a9831 --- /dev/null +++ b/projects/x-framework-core/src/lib/constants/x-translation.resources.ts @@ -0,0 +1,220 @@ +export enum XResourceIDs { + // + //#region App ... + app_name, + company, + //#endregion + + // + //#region Commons ... + id, + file_name, + // + error, + required, + + // + loading, + + // + username, + password, + + // + first_name, + last_name, + + // + verified, + unverified, + + // + thumbnail, + language, + + // + take_screenshot, + enable_capture_mode, + disable_capture_mode, + + // + be, + not_to_be, + enabled, + disabled, + banned, + un_banned, + //#endregion + + // + //#region File Types ... + audio, + video, + document, + cover_image, + image, + avatar, + unknown, + location, + //#endregion + + // + //#region Messages ... + are_you_sure, + success_msg, + locked_msg, + empty_content, + no_item, + drag_drop_here, + //#endregion + + // + //#region Components ... + map_dialog_title, + map_dialog_select_title, + color_picker_dialog_title, + //#endregion + + // + //#region Verbs ... + dismiss, + accept, + ok, + save, + cancel, + retry, + edit, + reset, + change, + start, + yes, + no, + verify, + confirm, + crop, + clear, + set, + select, + send, + show, + hide, + search, + delete, + clone, + select_all, + select_file, + select_all_group, + deselect_all, + deselect_all_group, + inverse_selection, + selected_location, + color, + colors, + preset_colors, + copy_to_clipboard, + // + upload, + import, + continue, + finish, + enable, + disable, + // + all, + new, + add, + update, + details, + translation, + + // + download, + list, + options, + descriptions, + + // + type, + + // + media, + medias, + + // + go, + input, + preview, + + // + close, + undo, + redo, + + // + refresh, + + // + submit, + + // + //#region Pagination ... + first, + last, + previous, + next, + back, + //#endregion + //#endregion + + // + //#region Themes Related ... + // + theme, + theme_mode, + // + theme_dark, + theme_light, + // + theme_name, + theme_name_description, + // + theme_clone_title, + //#endregion + + // + //#region Color Picker Related ... + input_color_title, + input_color_description, + //#endregion + + // + //#region Markdown Component ... + x_markdown_preview, + x_markdown_image, + x_markdown_link, + x_markdown_ol, + x_markdown_ul, + x_markdown_quote, + x_markdown_code, + x_markdown_heading, + x_markdown_italic, + x_markdown_bold, + x_markdown_guide, + //#endregion + + // + //#region XIDE Component ... + ide_mode, + ide_format, + //#endregion + + // + //#region Password Validation Errors ... + required_length, + required_unique_chars, + require_digit, + require_lowercase, + require_uppercase, + require_non_alphanumeric, + //#endregion +} diff --git a/projects/x-framework-core/src/lib/constants/x.constants.ts b/projects/x-framework-core/src/lib/constants/x.constants.ts new file mode 100644 index 0000000..5feb71b --- /dev/null +++ b/projects/x-framework-core/src/lib/constants/x.constants.ts @@ -0,0 +1 @@ +export const DEFAULT_JAVASCRIPT_VOID_HREF = 'javascript:void(0)'; diff --git a/projects/x-framework-core/src/lib/directives/x-clipboard.directive.ts b/projects/x-framework-core/src/lib/directives/x-clipboard.directive.ts new file mode 100644 index 0000000..8f99ec1 --- /dev/null +++ b/projects/x-framework-core/src/lib/directives/x-clipboard.directive.ts @@ -0,0 +1,47 @@ +import { + Input, + Output, + Directive, + HostListener, + EventEmitter +} from '@angular/core'; +import { isNullOrEmptyString } from '../tools/x-type-detect.tools'; + +@Directive({ + // tslint:disable-next-line:directive-selector + selector: '[xClipboard]' +}) +export class XClipboardDirective { + // + @Input() + clipData: string; + + @Output() + copied = new EventEmitter(); + + @HostListener('click', ['$event']) + whenClicked(event: MouseEvent) { + event.preventDefault(); + + // + if (isNullOrEmptyString(this.clipData)) { + return; + } + + // + const listener = (e: ClipboardEvent) => { + // + // tslint:disable-next-line:no-string-literal + const clipboard = e.clipboardData || window['clipboardData']; + clipboard.setData('text', this.clipData); + e.preventDefault(); + + this.copied.emit(this.clipData); + }; + + // + document.addEventListener('copy', listener, false); + document.execCommand('copy'); + document.removeEventListener('copy', listener, false); + } +} diff --git a/projects/x-framework-core/src/lib/directives/x-color.directive.ts b/projects/x-framework-core/src/lib/directives/x-color.directive.ts new file mode 100644 index 0000000..7456d36 --- /dev/null +++ b/projects/x-framework-core/src/lib/directives/x-color.directive.ts @@ -0,0 +1,98 @@ +import { + Input, + Renderer2, + OnChanges, + Directive, + ElementRef, + SimpleChanges, +} from '@angular/core'; +import { + getXColorClassName, + getXColorForegroundClassName, +} from '../tools/x-color.tools'; +import { keys } from '../tools/x-object.tools'; +import { XStandardType } from '../typings/x.typings'; +import { concatMap, map, first } from 'rxjs/operators'; +import { toObservable } from '../tools/x-observable.tools'; +import { isNullOrEmptyString } from '../tools/x-type-detect.tools'; +import { XColorIdentifier, XColor } from '../typings/x-color.typings'; + +/** + * this directive used to apply an specific color on an element ... + */ +@Directive({ + // tslint:disable-next-line:directive-selector + selector: '[xColor]', +}) +export class XColorDirective implements OnChanges { + // + private COLOR_CLASS: string; + + // + /** + * a color identifier to apply on element ... + */ + // tslint:disable-next-line:no-input-rename + @Input('xColor') + xColor: XStandardType; + + /** + * determines the selected color must apply on foreground or background of element + */ + @Input() + isForeground: XStandardType = false; + + // + constructor(private element: ElementRef, private renderer: Renderer2) {} + + // + ngOnChanges(changes: SimpleChanges) { + // + const changeKeys = keys(changes); + if (changeKeys.includes('xColor') || changeKeys.includes('isForeground')) { + this.handleColorChanges(); + } + } + + // + private handleColorChanges() { + // + toObservable(this.xColor) + .pipe( + concatMap((color) => { + return toObservable(this.isForeground).pipe( + map((isForeground) => { + return { + color, + isForeground, + }; + }) + ); + }), + first() + ) + .subscribe((info) => { + // + if (!info.color) { + info.color = XColor.Dark; + } + + // + const colorClassName = info.isForeground + ? getXColorForegroundClassName(info.color) + : getXColorClassName(info.color); + + // + if (!isNullOrEmptyString(this.COLOR_CLASS)) { + this.renderer.removeClass( + this.element.nativeElement, + this.COLOR_CLASS + ); + } + + // + this.COLOR_CLASS = colorClassName; + this.renderer.addClass(this.element.nativeElement, colorClassName); + }); + } +} diff --git a/projects/x-framework-core/src/lib/directives/x-droppable.directive.ts b/projects/x-framework-core/src/lib/directives/x-droppable.directive.ts new file mode 100644 index 0000000..32c7bcf --- /dev/null +++ b/projects/x-framework-core/src/lib/directives/x-droppable.directive.ts @@ -0,0 +1,235 @@ +import { + Input, + Output, + Renderer2, + Directive, + ElementRef, + EventEmitter, + HostListener, +} from '@angular/core'; +import { + getXColorClassName, + getXColorForegroundClassName, +} from '../tools/x-color.tools'; +import { hasChild } from '../tools/x-array.tools'; +import { XOneOrManyType } from '../typings/x.typings'; +import { isArray, isString } from '../tools/x-type-detect.tools'; +import { XColorIdentifier, XColor } from '../typings/x-color.typings'; +import { getExtension, isExistsExtension } from '../tools/x-file.tools'; + +@Directive({ + // tslint:disable-next-line:directive-selector + selector: '[xDroppable]', +}) +export class XDroppableDirective { + // + private DISABLED: boolean; + private COLOR_CLASSES: string[] = []; + + // + @Input() + disableFileCheck = false; + + @Input() + public allowedExtensions: XOneOrManyType = []; + + @Input() + public successColor: XColorIdentifier = XColor.Success; + + @Input() + public errorColor: XColorIdentifier = XColor.Danger; + + @Input() + public accentColor: XColorIdentifier = XColor.Secondary; + + @Input() + public supportsMultipleFiles = true; + + @Input() + public isForegroundColor = false; + + @Input() + set dragDisabled(value: boolean) { + // + if (value !== this.DISABLED) { + // + this.DISABLED = value; + if (!value) { + this.setBackgroundColor('transparent'); + } else { + this.setBackgroundColor(this.successColor); + } + } + } + + @Input() + set error(val: boolean) { + if (val) { + this.setBackgroundColor(this.errorColor); + return; + } + + if (this.DISABLED) { + return; + } + + this.setBackgroundColor('transparent'); + } + + @Output() + public filesChangeEmiter: EventEmitter = new EventEmitter(); + + constructor(private elementRef: ElementRef, private renderer: Renderer2) {} + + @HostListener('dragover', ['$event']) + public async onDragOver(event: any) { + // + event.preventDefault(); + event.stopPropagation(); + + // + if (this.DISABLED) { + return; + } + + // + await this.setBackgroundColor(this.accentColor); + } + + @HostListener('dragleave', ['$event']) + public async onDragLeave(event: any) { + // + event.preventDefault(); + event.stopPropagation(); + + // + if (this.DISABLED) { + return; + } + + // + await this.setBackgroundColor('transparent'); + } + + @HostListener('drop', ['$event']) + public async onDrop(event: DragEvent) { + event.preventDefault(); + event.stopPropagation(); + + // + if (this.DISABLED) { + return; + } + + // + const files = Array.from(event.dataTransfer.files); + if (files.length === 0) { + await this.setBackgroundColor(this.errorColor); + return; + } + + // + // Retrieve Allowen Extensions ... + let allowedExtensions: string[] = []; + if (isArray(this.allowedExtensions)) { + allowedExtensions = this.allowedExtensions as string[]; + } else if (isString(this.allowedExtensions)) { + allowedExtensions.push(this.allowedExtensions as string); + } + + // + if (!this.disableFileCheck) { + if (files.length === 1) { + // + const file = files[0]; + const ext = getExtension(file.name); + + // + let isAllowed = true; + + if (hasChild(allowedExtensions)) { + isAllowed = isExistsExtension(allowedExtensions, ext); + } + + // + if (isAllowed) { + // + await this.setBackgroundColor(this.successColor); + } else { + await this.setBackgroundColor(this.errorColor); + return; + } + + // + this.filesChangeEmiter.emit(file); + await this.setBackgroundColor('transparent'); + } else { + // + if (!this.supportsMultipleFiles) { + await this.setBackgroundColor(this.errorColor); + return; + } + + // + const isAllowedAll = !hasChild(allowedExtensions) + ? true + : files.every((file) => { + // + const ext = getExtension(file.name); + // + let isAllowed = true; + if (hasChild(allowedExtensions)) { + isAllowed = isExistsExtension(allowedExtensions, ext); + } + + // + return isAllowed; + }); + + // + if (isAllowedAll) { + // + await this.setBackgroundColor(this.successColor); + } else { + await this.setBackgroundColor(this.errorColor); + return; + } + + // + this.filesChangeEmiter.emit(files); + await this.setBackgroundColor('transparent'); + } + } else { + // + this.filesChangeEmiter.emit(files); + await this.setBackgroundColor('transparent'); + } + } + + private async setBackgroundColor(color: XColorIdentifier) { + // + if (!color) { + color = 'transparent'; + } + + // + const colorClassName = this.isForegroundColor + ? getXColorForegroundClassName(color) + : getXColorClassName(color); + + // + if (!this.COLOR_CLASSES.includes(colorClassName)) { + this.COLOR_CLASSES.push(colorClassName); + } + + // + if (hasChild(this.COLOR_CLASSES)) { + this.COLOR_CLASSES.forEach((clz) => { + this.renderer.removeClass(this.elementRef.nativeElement, clz); + }); + } + + // + this.renderer.addClass(this.elementRef.nativeElement, colorClassName); + } +} diff --git a/projects/x-framework-core/src/lib/directives/x-file-reader.directive.ts b/projects/x-framework-core/src/lib/directives/x-file-reader.directive.ts new file mode 100644 index 0000000..7b7dae8 --- /dev/null +++ b/projects/x-framework-core/src/lib/directives/x-file-reader.directive.ts @@ -0,0 +1,135 @@ +import { + Input, + Output, + OnInit, + OnChanges, + Directive, + OnDestroy, + EventEmitter, + SimpleChanges +} from '@angular/core'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { XFileType } from '../constants/x-file.enum'; +import { getFileType } from '../tools/x-file.tools'; +import { keys, nameof } from '../tools/x-object.tools'; +import { XFileReaderInputType } from '../typings/x.typings'; + +@Directive({ + // tslint:disable-next-line:directive-selector + selector: '[xFileReader]' +}) +export class XFileReaderDirective implements OnInit, OnChanges, OnDestroy { + // + private UNSUBSCRIBER = new Subject(); + private FILE_READER: FileReader; + + // + @Input('xFileReader') + file: XFileReaderInputType; + + @Input() + refreshFileReader = new Subject(); + + // + // tslint:disable-next-line:no-output-on-prefix + @Output() + onFileReaderError = new EventEmitter(); + + // + // tslint:disable-next-line:no-output-on-prefix + @Output() + onFileReaderLoad = new EventEmitter(); + + // + // tslint:disable-next-line:no-output-on-prefix + @Output() + onFileReaderProgress = new EventEmitter(); + + // + // tslint:disable-next-line:no-output-on-prefix + @Output() + onFileReaderFinish = new EventEmitter(); + + // + ngOnInit() { + this.refreshFileReader + .asObservable() + .pipe(takeUntil(this.UNSUBSCRIBER)) + .subscribe(() => { + this.handleStartReading(); + }); + } + + // + ngOnDestroy() { + this.UNSUBSCRIBER.next(); + this.UNSUBSCRIBER.complete(); + this.UNSUBSCRIBER = null; + } + + // + ngOnChanges(changes: SimpleChanges) { + // + const changeKeys = keys(changes); + const isFileChanged = changeKeys.includes( + nameof('file') + ); + if (isFileChanged) { + this.handleStartReading(); + } + } + + // + private async handleStartReading() { + // + await this.destroyReader(); + + // + const type = getFileType(this.file); + if (type !== XFileType.Image) { + return; + } + + // + this.FILE_READER = new FileReader(); + + // + this.FILE_READER.onerror = err => { + this.onFileReaderError.emit(err); + this.destroyReader(); + }; + + // + this.FILE_READER.onabort = err => { + this.onFileReaderError.emit(err); + this.destroyReader(); + }; + + // + this.FILE_READER.onloadstart = _ => this.onFileReaderLoad.emit(true); + this.FILE_READER.onprogress = ev => this.onFileReaderProgress.emit(ev); + this.FILE_READER.onloadend = _ => { + this.onFileReaderLoad.emit(false); + this.destroyReader(); + }; + + // + this.FILE_READER.onload = _ => + this.onFileReaderFinish.emit(this.FILE_READER.result); + + // + this.FILE_READER.readAsDataURL(this.file as Blob); + } + + private async destroyReader() { + // + if (!this.FILE_READER) { + return; + } + + // + this.FILE_READER.abort(); + this.FILE_READER = null; + } +} diff --git a/projects/x-framework-core/src/lib/directives/x-focus-handler.directive.ts b/projects/x-framework-core/src/lib/directives/x-focus-handler.directive.ts new file mode 100644 index 0000000..6e59c89 --- /dev/null +++ b/projects/x-framework-core/src/lib/directives/x-focus-handler.directive.ts @@ -0,0 +1,107 @@ +import { + Input, + Directive, + ElementRef, + HostListener, + ChangeDetectorRef, +} from '@angular/core'; +import { toArray } from '../tools/x-array.tools'; +import { toPromise } from '../tools/x-observable.tools'; +import { XOneOrManyType, XStandardType } from '../typings/x.typings'; + +@Directive({ + // tslint:disable-next-line:directive-selector + selector: '[xFocusHandler]', +}) +export class XFocusedHandlerDirective { + // + //#region Props ... + @Input() + enableHandler: XStandardType = false; + + @Input() + appliedClassNames: XStandardType> = 'x-focused'; + //#endregion + + // + //#region Constructor ... + constructor( + public el: ElementRef, + public changeDetector: ChangeDetectorRef + ) {} + //#endregion + + // + //#region HostListener Handlers ... + @HostListener('focus', ['$event']) + async whenFocused(event: any) { + // + const isEnable = await this.isEnable(); + if (!isEnable) { + return; + } + + // + const el = this.getElement(); + if (!el) { + return; + } + + // + const appliedClasses = await this.getClasses(); + + // + appliedClasses.forEach((clz) => { + el.classList.add(clz); + }); + + // + this.changeDetector.detectChanges(); + } + + @HostListener('blur', ['$event']) + async whenBlured(event: any) { + // + const isEnable = await this.isEnable(); + if (!isEnable) { + return; + } + + // + const el = this.getElement(); + if (!el) { + return; + } + + // + const appliedClasses = await this.getClasses(); + + // + appliedClasses.forEach((clz) => { + el.classList.remove(clz); + }); + + // + this.changeDetector.detectChanges(); + } + //#endregion + + // + //#region Private ... + private async isEnable() { + return await toPromise(this.enableHandler); + } + + private async getClasses() { + // + const classType = await toPromise(this.appliedClassNames); + return toArray(classType); + } + + private getElement() { + return this.el && this.el.nativeElement + ? (this.el.nativeElement as HTMLElement) + : null; + } + //#endregion +} diff --git a/projects/x-framework-core/src/lib/directives/x-focus.directive.ts b/projects/x-framework-core/src/lib/directives/x-focus.directive.ts new file mode 100644 index 0000000..3fe5f23 --- /dev/null +++ b/projects/x-framework-core/src/lib/directives/x-focus.directive.ts @@ -0,0 +1,91 @@ +import { + Input, + Directive, + OnDestroy, + OnChanges, + ElementRef, + AfterViewInit, + SimpleChanges, + AfterContentInit, +} from '@angular/core'; +import { takeUntil } from 'rxjs/operators'; +import { Subject, Subscription } from 'rxjs'; +import { nameof } from '../tools/x-object.tools'; +import { isNullOrUndefined } from '../tools/x-type-detect.tools'; + +@Directive({ + // tslint:disable-next-line:directive-selector + selector: '[xFocus]', +}) +export class XFocusDirective + implements AfterViewInit, AfterContentInit, OnChanges, OnDestroy +{ + // + private subscription: Subscription; + private ngUnsubscribe = new Subject(); + + // + @Input('xFocus') + reFocus = new Subject(); + + constructor(private element: ElementRef) {} + + // + //#region LifeCycles ... + ngAfterViewInit() { + this.initFocusHandler(); + } + + ngAfterContentInit() { + this.doFocus(); + } + + ngOnChanges(changes: SimpleChanges): void { + // + if (isNullOrUndefined(changes)) { + return; + } + + // + const changeKeys = Object.keys(changes); + const isReFocusChanged = changeKeys.includes( + nameof('reFocus') + ); + if (isReFocusChanged) { + this.initFocusHandler(); + } + } + + ngOnDestroy() { + // + this.ngUnsubscribe.next(); + this.ngUnsubscribe.complete(); + } + //#endregion + + private doFocus() { + setTimeout(() => this.element.nativeElement.focus(), 500); + } + + private initFocusHandler() { + // + if (!isNullOrUndefined(this.subscription)) { + // + this.subscription.unsubscribe(); + this.subscription = undefined; + } + + // + if (isNullOrUndefined(this.reFocus)) { + this.reFocus = new Subject(); + } + + // + this.subscription = this.reFocus + .asObservable() + .pipe(takeUntil(this.ngUnsubscribe)) + .subscribe(() => { + this.doFocus(); + }); + } +} diff --git a/projects/x-framework-core/src/lib/directives/x-formatter.directive.ts b/projects/x-framework-core/src/lib/directives/x-formatter.directive.ts new file mode 100644 index 0000000..8c2c5d3 --- /dev/null +++ b/projects/x-framework-core/src/lib/directives/x-formatter.directive.ts @@ -0,0 +1,61 @@ +import { + Input, + OnInit, + Directive, + ElementRef, + HostListener, +} from '@angular/core'; +import { XFormatterOption } from '../typings/x.typings'; +import { applyFormat, clearFormat } from '../tools/x-formatter.tools'; + +@Directive({ + // tslint:disable-next-line:directive-selector + selector: '[xFormatter]', +}) +export class XFormatterDirective implements OnInit { + // + private el: HTMLInputElement; + + // + @Input('xFormatter') + options: XFormatterOption; + + @Input() + clearLocale = true; + + // + constructor(private elementRef: ElementRef) { + this.el = this.elementRef.nativeElement; + } + + // + ngOnInit() { + this.el.value = this.format(this.el.value); + } + + // + @HostListener('focus', ['$event.target.value']) + onFocus(value: any) { + this.el.value = this.clear(value); + } + + // + @HostListener('blur', ['$event.target.value']) + onBlur(value: any) { + this.el.value = this.format(value); + } + + // + private format(value: any) { + return applyFormat(this.options, value ? value.toString() : ''); + } + + // + private clear(value: any) { + return clearFormat( + this.options, + this.clearLocale, + value ? value.toString() : '' + ); + } +} diff --git a/projects/x-framework-core/src/lib/directives/x-mouse-handler.directive.ts b/projects/x-framework-core/src/lib/directives/x-mouse-handler.directive.ts new file mode 100644 index 0000000..b7d75d9 --- /dev/null +++ b/projects/x-framework-core/src/lib/directives/x-mouse-handler.directive.ts @@ -0,0 +1,187 @@ +import { + Input, + OnInit, + Output, + Directive, + OnDestroy, + OnChanges, + ElementRef, + HostListener, + EventEmitter, + AfterViewInit, + SimpleChanges, + AfterContentInit, +} from '@angular/core'; +import { XStandardType } from '../typings/x.typings'; +import { toPromise } from '../tools/x-observable.tools'; +import { isNullOrUndefined } from '../tools/x-type-detect.tools'; + +/** + * this is a directive which bind to mouse/touch events and react based on them ... + */ +@Directive({ + // tslint:disable-next-line:directive-selector + selector: '[xMouseHandler]', +}) +export class XMouseHandlerDirective + implements OnInit, OnChanges, AfterViewInit, AfterContentInit, OnDestroy { + // + //#region Props ... + @Input() + enableHandler: XStandardType = false; + + @Output() + started = new EventEmitter(); + + @Output() + moved = new EventEmitter(); + + @Output() + finished = new EventEmitter(); + + // tslint:disable-next-line:no-output-on-prefix + @Output() + onWindowStarted = new EventEmitter(); + + // tslint:disable-next-line:no-output-on-prefix + @Output() + onWindowMoved = new EventEmitter(); + + // tslint:disable-next-line:no-output-on-prefix + @Output() + onWindowFinished = new EventEmitter(); + //#endregion + + // + //#region Constructor ... + constructor(private el: ElementRef) {} + //#endregion + + // + //#region LifeCycles ... + ngOnInit() {} + + ngOnChanges(changes: SimpleChanges) {} + + ngAfterViewInit() {} + + ngAfterContentInit() {} + + ngOnDestroy() {} + //#endregion + + // + //#region Host Listeners ... + // + //#region Element Listeners ... + @HostListener('document:mousedown', ['$event']) + @HostListener('document:touchstart', ['$event']) + async whenMoveStart(event: MouseEvent) { + // + const isEnable = await this.isEnable(); + if (!isEnable) { + return; + } + + // + if (!this.isTargetMatched(event)) { + return; + } + + // + this.started.emit(event); + } + + @HostListener('document:mousemove', ['$event']) + @HostListener('document:touchmove', ['$event']) + async whenMove(event: MouseEvent) { + // + const isEnable = await this.isEnable(); + if (!isEnable) { + return; + } + + // + if (!this.isTargetMatched(event)) { + return; + } + + // + this.moved.emit(event); + } + + @HostListener('document:mouseup', ['$event']) + @HostListener('document:touchend', ['$event']) + async whenMoveFinished(event: MouseEvent) { + // + const isEnable = await this.isEnable(); + if (!isEnable) { + return; + } + + // + if (!this.isTargetMatched(event)) { + return; + } + + // + this.finished.emit(event); + } + //#endregion + + // + //#region Window Listeners ... + @HostListener('window:mousedown', ['$event']) + @HostListener('window:touchstart', ['$event']) + async whenWindowMoveStart(event: MouseEvent) { + // + const isEnable = await this.isEnable(); + if (!isEnable) { + return; + } + + // + this.onWindowStarted.emit(event); + } + + @HostListener('window:mousemove', ['$event']) + @HostListener('window:touchmove', ['$event']) + async whenWindowMove(event: MouseEvent) { + // + const isEnable = await this.isEnable(); + if (!isEnable) { + return; + } + + // + this.onWindowMoved.emit(event); + } + + @HostListener('window:mouseup', ['$event']) + @HostListener('window:touchend', ['$event']) + async whenWindowMoveFinished(event: MouseEvent) { + // + const isEnable = await this.isEnable(); + if (!isEnable) { + return; + } + + // + this.onWindowFinished.emit(event); + } + //#endregion + //#endregion + + // + //#region Private ... + private async isEnable() { + return await toPromise(this.enableHandler); + } + + private isTargetMatched(event: MouseEvent) { + return !isNullOrUndefined(event) + ? (this.el.nativeElement.contains(event.target) as boolean) + : false; + } + //#endregion +} diff --git a/projects/x-framework-core/src/lib/directives/x-on-enter.directive.ts b/projects/x-framework-core/src/lib/directives/x-on-enter.directive.ts new file mode 100644 index 0000000..4fa870e --- /dev/null +++ b/projects/x-framework-core/src/lib/directives/x-on-enter.directive.ts @@ -0,0 +1,18 @@ +import { Directive, Output, EventEmitter, HostListener } from '@angular/core'; + +@Directive({ + // tslint:disable-next-line:directive-selector + selector: '[xOnEnter]' +}) +export class XOnEnterDirective { + // + @Output('xOnEnter') + listener: EventEmitter = new EventEmitter(); + + // + @HostListener('keyup.enter', ['$event']) + onEnterPressed(event: any) { + // tslint:disable-next-line: deprecation + this.listener.emit(event); + } +} diff --git a/projects/x-framework-core/src/lib/directives/x-on-escape.directive.ts b/projects/x-framework-core/src/lib/directives/x-on-escape.directive.ts new file mode 100644 index 0000000..6dca602 --- /dev/null +++ b/projects/x-framework-core/src/lib/directives/x-on-escape.directive.ts @@ -0,0 +1,18 @@ +import { Directive, Output, EventEmitter, HostListener } from '@angular/core'; + +@Directive({ + // tslint:disable-next-line:directive-selector + selector: '[xOnEscape]' +}) +export class XOnEscapeDirective { + // + @Output('xOnEscape') + listener: EventEmitter = new EventEmitter(); + + // + @HostListener('keyup.esc', ['$event']) + onEscapePressed(event: any) { + // tslint:disable-next-line: deprecation + this.listener.emit(event); + } +} diff --git a/projects/x-framework-core/src/lib/directives/x-on-outside-click.directive.ts b/projects/x-framework-core/src/lib/directives/x-on-outside-click.directive.ts new file mode 100644 index 0000000..f7c8435 --- /dev/null +++ b/projects/x-framework-core/src/lib/directives/x-on-outside-click.directive.ts @@ -0,0 +1,29 @@ +import { + Output, + Directive, + ElementRef, + EventEmitter, + HostListener, +} from '@angular/core'; + +@Directive({ + // tslint:disable-next-line:directive-selector + selector: '[xOnOutsideClicked]', +}) +export class XOutsideClickDirective { + // + @Output('xOnOutsideClicked') + listener: EventEmitter = new EventEmitter(); + + // + constructor(private elementRef: ElementRef) {} + + @HostListener('document:click', ['$event.target']) + public onClick(targetElement: any) { + // + const clickedInside = this.elementRef.nativeElement.contains(targetElement); + if (!clickedInside) { + this.listener.emit(); + } + } +} diff --git a/projects/x-framework-core/src/lib/directives/x-render.directive.ts b/projects/x-framework-core/src/lib/directives/x-render.directive.ts new file mode 100644 index 0000000..4aca9b8 --- /dev/null +++ b/projects/x-framework-core/src/lib/directives/x-render.directive.ts @@ -0,0 +1,63 @@ +import { + Directive, + Renderer2, + ElementRef, + RendererStyleFlags2 +} from '@angular/core'; + +@Directive({ + // tslint:disable-next-line:directive-selector + selector: '[xRender]' +}) +export class XRenderDirective { + // + constructor(private renderer: Renderer2, private el: ElementRef) {} + + addClass(clz: string) { + this.renderer.addClass(this.el.nativeElement, clz); + } + + removeClass(clz: string) { + this.renderer.removeClass(this.el.nativeElement, clz); + } + + createElement(name: string, nameSpace?: string) { + return this.renderer.createElement(name, nameSpace); + } + + createText(value: string) { + return this.renderer.createText(value); + } + + appendChild(parent: any, newChild: any) { + this.renderer.appendChild(parent, newChild); + } + + appendChildToElement(child: any) { + this.renderer.appendChild(this.el.nativeElement, child); + } + + setAttribute(name: string, value: string, namespace?: string | null) { + this.renderer.setAttribute(this.el.nativeElement, name, value, namespace); + } + + removeAttribute(name: string, namespace?: string | null) { + this.renderer.removeAttribute(this.el.nativeElement, name, namespace); + } + + setStyle(style: string, value: any, flags?: RendererStyleFlags2) { + this.renderer.setStyle(this.el.nativeElement, style, value, flags); + } + + removeStyle(style: string, flags?: RendererStyleFlags2) { + this.renderer.removeStyle(this.el.nativeElement, style, flags); + } + + setProperty(name: string, value: any) { + this.renderer.setProperty(this.el.nativeElement, name, value); + } + + setValue(node: any, value: string) { + this.renderer.setValue(node, value); + } +} diff --git a/projects/x-framework-core/src/lib/directives/x-scroll-behavior.directive.ts b/projects/x-framework-core/src/lib/directives/x-scroll-behavior.directive.ts new file mode 100644 index 0000000..e95c565 --- /dev/null +++ b/projects/x-framework-core/src/lib/directives/x-scroll-behavior.directive.ts @@ -0,0 +1,329 @@ +import { + Input, + OnInit, + Output, + Directive, + Renderer2, + OnDestroy, + OnChanges, + EventEmitter, + HostListener, + AfterViewInit, + SimpleChanges, +} from '@angular/core'; +import { Subject, Subscription } from 'rxjs'; +import { hasChild } from '../tools/x-array.tools'; +import { keys, nameof } from '../tools/x-object.tools'; +import { isArray, isString } from '../tools/x-type-detect.tools'; + +// +//#region Definitions ... +export interface XBehaviourEvent { + type: XBehaviourEventType; + event: any; + elements: Element[]; +} + +export type XBehaviourEventType = 'scrollStart' | 'scrollEnd' | 'scrolling'; +//#endregion + +@Directive({ + // tslint:disable-next-line:directive-selector + selector: '[xScrollBehaviour]', +}) +export class XScrollBehaviourDirective + implements OnInit, OnChanges, OnDestroy, AfterViewInit +{ + // + private elementsRef: Element[] = []; + private xUnSubscriber = new Subject(); + + // + @Output() + scrollStarted = new EventEmitter(); + + @Output() + scrollEnded = new EventEmitter(); + + @Output() + scrolling = new EventEmitter(); + + // + @Input() + scrollStartClasses: string | string[]; + + @Input() + scrollEndClasses: string | string[]; + + @Input() + scrollingClasses: string | string[]; + + // + private ACTION_PROVIDER_SUBSCRIPTION: Subscription; + + @Input() + scrollingActionProvider = new Subject(); + + // + @Input('xScrollBehaviour') + elements: string | string[]; + + // + @HostListener('ionScrollStart', ['$event']) + async onScrollStart(event: any) { + // + await this.prepareScroller(); + + // + await this.removeClass(this.scrollEndClasses); + await this.addClass(this.scrollStartClasses); + + // + const eventModel: XBehaviourEvent = { + type: 'scrollStart', + event, + elements: this.elementsRef, + }; + + // + this.scrollStarted.emit(eventModel); + } + + @HostListener('ionScrollEnd', ['$event']) + async onScrollEnd(event: any) { + // + await this.prepareScroller(); + + // + await this.removeClass(this.scrollStartClasses); + await this.removeClass(this.scrollingClasses); + await this.addClass(this.scrollEndClasses); + + // + const eventModel: XBehaviourEvent = { + type: 'scrollEnd', + event, + elements: this.elementsRef, + }; + + // + this.scrollEnded.emit(eventModel); + } + + @HostListener('ionScroll', ['$event']) + async onScroll(event: any) { + // + await this.prepareScroller(); + + // + await this.addClass(this.scrollingClasses); + + // + const eventModel: XBehaviourEvent = { + type: 'scrolling', + event, + elements: this.elementsRef, + }; + + // + this.scrolling.emit(eventModel); + } + + // + constructor(public renderer: Renderer2) {} + + // + async ngOnInit() { + // + await this.prepareScroller(); + } + + async ngAfterViewInit() { + // + await this.prepareScroller(); + } + + ngOnChanges(changes: SimpleChanges) { + // + const changeKeys = keys(changes); + + // + //#region ActionProvider Changed ... + const isActionProviderChanged = changeKeys.includes( + nameof('scrollingActionProvider') + ); + if (isActionProviderChanged) { + this.registerActionProvider(); + } + //#endregion + } + + ngOnDestroy() { + // + this.unregisterActionProvider(); + + // + this.xUnSubscriber.next(); + this.xUnSubscriber.complete(); + } + + // + private getType() { + // + if (!this.elements) { + return 'unknown'; + } + + // + if (isArray(this.elements)) { + return 'array'; + } else { + return 'single'; + } + } + + private registerActionProvider() { + // + this.unregisterActionProvider(); + + // + if (!this.scrollingActionProvider) { + this.scrollingActionProvider = new Subject(); + } + + // + this.ACTION_PROVIDER_SUBSCRIPTION = this.scrollingActionProvider + .asObservable() + .subscribe(async (model) => { + // + if (!model) { + return; + } + + // + switch (model) { + // + case 'scrollStart': + await this.onScrollStart(null); + break; + + // + case 'scrollEnd': + await this.onScrollEnd(null); + break; + + // + case 'scrolling': + await this.onScroll(null); + break; + } + }); + } + + private unregisterActionProvider() { + // + if (!this.ACTION_PROVIDER_SUBSCRIPTION) { + return; + } + + // + this.ACTION_PROVIDER_SUBSCRIPTION.unsubscribe(); + this.ACTION_PROVIDER_SUBSCRIPTION = null; + } + + private async prepareScroller() { + // + await this.generateElementsRef(); + await this.addTransition(); + + // + this.registerActionProvider(); + } + + private async generateElementsRef() { + // + this.elementsRef = []; + + // + const inputType = this.getType(); + if (inputType === 'unknown') { + return []; + } else if (inputType === 'single') { + // + const elements = document.querySelectorAll(this.elements as string); + if (elements.length > 0) { + elements.forEach((e) => { + this.elementsRef.push(e); + }); + } + } else if (inputType === 'array') { + // + (this.elements as string[]).forEach((eSelector) => { + // + const elements = document.querySelectorAll(eSelector); + if (elements.length > 0) { + elements.forEach((e) => { + this.elementsRef.push(e); + }); + } + }); + } + + // + return this.elementsRef; + } + + private async addClass(cls: string | string[]) { + // + if (!hasChild(this.elementsRef)) { + return; + } + + // + const clsList = isString(cls) ? [cls as string] : (cls as string[]); + if (!hasChild(clsList)) { + return; + } + + // + this.elementsRef.forEach((e) => { + e.classList.add(...clsList); + }); + } + + private async removeClass(cls: string | string[]) { + // + if (!hasChild(this.elementsRef)) { + return; + } + + // + const clsList = isString(cls) ? [cls as string] : (cls as string[]); + if (!hasChild(clsList)) { + return; + } + + // + this.elementsRef.forEach((e) => { + e.classList.remove(...clsList); + }); + } + + private async addTransition(descriptor?: string) { + // + if (!descriptor) { + descriptor = 'transform 500ms, opacity 250ms'; + } + + // + if (!hasChild(this.elementsRef)) { + return; + } + + // + this.elementsRef.forEach((e) => { + this.renderer.setStyle(e, '-webkit-transition', descriptor); + this.renderer.setStyle(e, 'transition', descriptor); + }); + } +} diff --git a/projects/x-framework-core/src/lib/directives/x-slider.directive.ts b/projects/x-framework-core/src/lib/directives/x-slider.directive.ts new file mode 100644 index 0000000..a832aee --- /dev/null +++ b/projects/x-framework-core/src/lib/directives/x-slider.directive.ts @@ -0,0 +1,118 @@ +import { + Input, + Output, + Directive, + ElementRef, + HostListener, + EventEmitter, +} from '@angular/core'; + +export interface XSliderValueEventModel { + s?: number; + v?: number; + rg?: number; + rgX?: number; + rgY?: number; +} + +@Directive({ + // tslint:disable-next-line:directive-selector + selector: '[xSlider]', +}) +export class XSliderDirective { + // + @Input() + rgX: number; + + @Input() + rgY: number; + + // + @Output() + newValue = new EventEmitter(); + + // + private listenerMove: any = (event: any) => { + this.move(event); + // tslint:disable-next-line:semicolon + }; + + // + private listenerStop: any = () => { + this.stop(); + // tslint:disable-next-line:semicolon + }; + + // + constructor(private el: ElementRef) {} + + setCursor(event: any) { + // + const height = this.el.nativeElement.offsetHeight; + const width = this.el.nativeElement.offsetWidth; + const x = Math.max(0, Math.min(this.getX(event), width)); + const y = Math.max(0, Math.min(this.getY(event), height)); + + // + if (this.rgX !== undefined && this.rgY !== undefined) { + // + this.newValue.emit({ + s: x / width, + v: 1 - y / height, + rgX: this.rgX, + rgY: this.rgY, + }); + } else if (this.rgX === undefined && this.rgY !== undefined) { + // + // ready to use vertical sliders + this.newValue.emit({ v: y / height, rg: this.rgY }); + } else { + // + this.newValue.emit({ v: x / width, rg: this.rgX }); + } + } + + move(event: any) { + event.preventDefault(); + this.setCursor(event); + } + + @HostListener('mousedown', ['$event']) + @HostListener('touchstart', ['$event']) + start(event: any) { + // + this.setCursor(event); + + // + document.addEventListener('mousemove', this.listenerMove); + document.addEventListener('touchmove', this.listenerMove); + document.addEventListener('mouseup', this.listenerStop); + document.addEventListener('touchend', this.listenerStop); + } + + stop() { + // + document.removeEventListener('mousemove', this.listenerMove); + document.removeEventListener('touchmove', this.listenerMove); + document.removeEventListener('mouseup', this.listenerStop); + document.removeEventListener('touchend', this.listenerStop); + } + + getX(event: any): number { + // + return ( + (event.pageX !== undefined ? event.pageX : event.touches[0].pageX) - + this.el.nativeElement.getBoundingClientRect().left - + window.pageXOffset + ); + } + + getY(event: any): number { + // + return ( + (event.pageY !== undefined ? event.pageY : event.touches[0].pageY) - + this.el.nativeElement.getBoundingClientRect().top - + window.pageYOffset + ); + } +} diff --git a/projects/x-framework-core/src/lib/directives/x-stop-click-propagation.directive.ts b/projects/x-framework-core/src/lib/directives/x-stop-click-propagation.directive.ts new file mode 100644 index 0000000..b1efd09 --- /dev/null +++ b/projects/x-framework-core/src/lib/directives/x-stop-click-propagation.directive.ts @@ -0,0 +1,12 @@ +import { Directive, HostListener } from '@angular/core'; + +@Directive({ + // tslint:disable-next-line:directive-selector + selector: '[xStopClickPropagation]' +}) +export class XStopClickPropagationDirective { + @HostListener('click', ['$event']) + public onClick(event: any): void { + event.stopPropagation(); + } +} diff --git a/projects/x-framework-core/src/lib/directives/x-var.directive.ts b/projects/x-framework-core/src/lib/directives/x-var.directive.ts new file mode 100644 index 0000000..632ad43 --- /dev/null +++ b/projects/x-framework-core/src/lib/directives/x-var.directive.ts @@ -0,0 +1,54 @@ +import { + isNullOrUndefined, + isNullOrEmptyString, +} from '../tools/x-type-detect.tools'; +import { keys } from '../tools/x-object.tools'; +import { hasChild } from '../tools/x-array.tools'; +import { Input, Directive, ChangeDetectorRef } from '@angular/core'; + +export interface XVarModel { + name: string; + value: any; +} + +@Directive({ + selector: '[xVar]', + exportAs: 'xVar', +}) +export class XVarDirective { + // + private context: any = {}; + + // + @Input('xVar') + set xVar(value: XVarModel) { + // + this.context[this.prepareVarName(value.name)] = value; + this.changeDetector.detectChanges(); + } + + constructor(private changeDetector: ChangeDetectorRef) {} + + hasVar(name: string) { + return ( + !isNullOrEmptyString(name) && + !isNullOrUndefined(this.context) && + hasChild(keys(this.context)) && + !isNullOrUndefined(this.context[name]) + ); + } + + getVar(name: string) { + return this.hasVar(this.prepareVarName(name)) + ? this.context[this.prepareVarName(name)] + : undefined; + } + + getVars() { + return this.context; + } + + private prepareVarName(name: string) { + return `xVar_${name}`; + } +} diff --git a/projects/x-framework-core/src/lib/guards/x-can-deactivate.guard.ts b/projects/x-framework-core/src/lib/guards/x-can-deactivate.guard.ts new file mode 100644 index 0000000..e26c400 --- /dev/null +++ b/projects/x-framework-core/src/lib/guards/x-can-deactivate.guard.ts @@ -0,0 +1,30 @@ +import { + CanDeactivate, + RouterStateSnapshot, + ActivatedRouteSnapshot +} from '@angular/router'; +import { Observable } from 'rxjs'; +import { Injectable } from '@angular/core'; + +export interface XCanDeactivate { + canDeactivate: ( + component: XCanDeactivate, + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot + ) => Observable | Promise | boolean; +} + +@Injectable() +export class XCanDeactivateGuard implements CanDeactivate { + canDeactivate( + component: XCanDeactivate, + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot + ) { + const url: string = state.url; + + return component.canDeactivate + ? component.canDeactivate(component, route, state) + : true; + } +} diff --git a/projects/x-framework-core/src/lib/models/x-base-dto.ts b/projects/x-framework-core/src/lib/models/x-base-dto.ts new file mode 100644 index 0000000..506b8d9 --- /dev/null +++ b/projects/x-framework-core/src/lib/models/x-base-dto.ts @@ -0,0 +1,61 @@ +import { isSameObject } from '../tools/x-object.tools'; + +// tslint:disable-next-line:no-empty-interface +export interface XBaseDto {} + +export abstract class XBaseDtoHelper { + // + //#region Abstracts ... + abstract defaultValues: T; + abstract get values(): T; + abstract prepareModelFields(dto?: T): T; + abstract fill(dto?: T): void; + //#endregion + + // + //#region Constructor ... + constructor(dto?: T) { + // + this.fill(this.prepareModelFields(dto)); + } + //#endregion + + // + //#region Actions ... + public isDefaultModel(dto?: T): boolean { + // + if (!dto) { + dto = { + ...this.values, + }; + } + + // + const result = isSameObject( + this.prepareModelFields(dto), + this.defaultValues + ); + + // + return result; + } + //#endregion +} + +export interface XBaseNumberIDDto { + id: number; +} + +// tslint:disable-next-line: no-empty-interface +export interface XBaseRequestDto {} + +export interface XBaseRangeRequestDto extends XBaseRequestDto { + items: Array; +} + +// tslint:disable-next-line:no-empty-interface +export interface XBaseResponseDto {} + +export interface XBaseStorableDto extends XBaseDto { + id?: T +} \ No newline at end of file diff --git a/projects/x-framework-core/src/lib/models/x-base.indexed.dto.ts b/projects/x-framework-core/src/lib/models/x-base.indexed.dto.ts new file mode 100644 index 0000000..d458101 --- /dev/null +++ b/projects/x-framework-core/src/lib/models/x-base.indexed.dto.ts @@ -0,0 +1,10 @@ +import { XBaseDto } from './x-base-dto'; + +export interface XBaseIndexedDto extends XBaseDto { + item: T; + key: TKey; + index: Tindex; +} + +export interface XBaseIntIndexedDto + extends XBaseIndexedDto {} diff --git a/projects/x-framework-core/src/lib/models/x-grouped-data.dto.ts b/projects/x-framework-core/src/lib/models/x-grouped-data.dto.ts new file mode 100644 index 0000000..9a74068 --- /dev/null +++ b/projects/x-framework-core/src/lib/models/x-grouped-data.dto.ts @@ -0,0 +1,7 @@ +import { XBaseDto } from "./x-base-dto"; + +export interface XGroupedDataDto extends XBaseDto { + groupedBy: TGroupedy; + members: Array; + timeStamp: Date; +} diff --git a/projects/x-framework-core/src/lib/modules/x-directives.module.ts b/projects/x-framework-core/src/lib/modules/x-directives.module.ts new file mode 100644 index 0000000..5874159 --- /dev/null +++ b/projects/x-framework-core/src/lib/modules/x-directives.module.ts @@ -0,0 +1,106 @@ +import { NgModule } from '@angular/core'; +import { IonicModule } from '@ionic/angular'; +import { FormsModule } from '@angular/forms'; +import { XAsPipe } from '../pipes/x-as.pipe'; +import { XLogPipe } from '../pipes/x-log.pipe'; +import { CommonModule } from '@angular/common'; +import { XSafeHtmlPipe } from '../pipes/x-safe-html.pipe'; +import { XFormatterPipe } from '../pipes/x-formatter.pipe'; +import { XVarDirective } from '../directives/x-var.directive'; +import { XFocusDirective } from '../directives/x-focus.directive'; +import { XColorDirective } from '../directives/x-color.directive'; +import { XRenderDirective } from '../directives/x-render.directive'; +import { XSliderDirective } from '../directives/x-slider.directive'; +import { XOnEnterDirective } from '../directives/x-on-enter.directive'; +import { XValidUrlDirective } from '../validators/x-valid-url.directive'; +import { XOnEscapeDirective } from '../directives/x-on-escape.directive'; +import { XDroppableDirective } from '../directives/x-droppable.directive'; +import { XClipboardDirective } from '../directives/x-clipboard.directive'; +import { XFormatterDirective } from '../directives/x-formatter.directive'; +import { XValueMatchDirective } from '../validators/x-value-match.directive'; +import { XValidEmailDirective } from '../validators/x-valid-email.directive'; +import { XFileReaderDirective } from '../directives/x-file-reader.directive'; +import { XMouseHandlerDirective } from '../directives/x-mouse-handler.directive'; +import { XFocusedHandlerDirective } from '../directives/x-focus-handler.directive'; +import { XOutsideClickDirective } from '../directives/x-on-outside-click.directive'; +import { XScrollBehaviourDirective } from '../directives/x-scroll-behavior.directive'; +import { XValidPhoneNumberDirective } from '../validators/x-valid-phone-number.directive'; +import { XStopClickPropagationDirective } from '../directives/x-stop-click-propagation.directive'; + +@NgModule({ + declarations: [ + // + // Directives + XVarDirective, + XFocusDirective, + XColorDirective, + XSliderDirective, + XRenderDirective, + XOnEnterDirective, + XOnEscapeDirective, + XFormatterDirective, + XClipboardDirective, + XDroppableDirective, + XFileReaderDirective, + XOutsideClickDirective, + XMouseHandlerDirective, + XFocusedHandlerDirective, + XScrollBehaviourDirective, + XStopClickPropagationDirective, + + // + // Pipes ... + XAsPipe, + XLogPipe, + XSafeHtmlPipe, + XFormatterPipe, + + // + // Validators ... + XValidUrlDirective, + XValueMatchDirective, + XValidEmailDirective, + XValidPhoneNumberDirective, + ], + imports: [CommonModule, IonicModule, FormsModule], + exports: [ + // + IonicModule, + FormsModule, + CommonModule, + + // + // Directives + XVarDirective, + XFocusDirective, + XColorDirective, + XSliderDirective, + XRenderDirective, + XOnEnterDirective, + XOnEscapeDirective, + XFormatterDirective, + XDroppableDirective, + XClipboardDirective, + XFileReaderDirective, + XOutsideClickDirective, + XMouseHandlerDirective, + XFocusedHandlerDirective, + XScrollBehaviourDirective, + XStopClickPropagationDirective, + + // + // Pipes ... + XAsPipe, + XLogPipe, + XSafeHtmlPipe, + XFormatterPipe, + + // + // Validators ... + XValidUrlDirective, + XValueMatchDirective, + XValidEmailDirective, + XValidPhoneNumberDirective, + ], +}) +export class XDirectivesModule {} diff --git a/projects/x-framework-core/src/lib/pipes/x-as.pipe.ts b/projects/x-framework-core/src/lib/pipes/x-as.pipe.ts new file mode 100644 index 0000000..f479af5 --- /dev/null +++ b/projects/x-framework-core/src/lib/pipes/x-as.pipe.ts @@ -0,0 +1,18 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'xAs', + pure: true, +}) +export class XAsPipe implements PipeTransform { + /** + * for casting a dynamic template var to specific type ... + * + * @param value the pure template var ... + * @param _type the type which required to cast value to it ... + * @returns a type casted var ... + */ + transform(value: any, _type: (new (...args: any[]) => T) | T): T { + return value as T; + } +} diff --git a/projects/x-framework-core/src/lib/pipes/x-formatter.pipe.ts b/projects/x-framework-core/src/lib/pipes/x-formatter.pipe.ts new file mode 100644 index 0000000..d3312c1 --- /dev/null +++ b/projects/x-framework-core/src/lib/pipes/x-formatter.pipe.ts @@ -0,0 +1,39 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { applyFormat, clearFormat } from '../tools/x-formatter.tools'; +import { XFormatterOption, XFormatterType } from '../typings/x.typings'; + +@Pipe({ + name: 'xFormatter', + pure: true, +}) +export class XFormatterPipe implements PipeTransform { + // + private defaultFormatterOption: XFormatterOption = { + type: XFormatterType.ApplyLocale, + }; + + // + transform(value: any, options?: XFormatterOption): string { + return this.format(options, value); + } + + // + clearFormat(value: any, options?: XFormatterOption, clearLocale = false) { + // + options = { + ...this.defaultFormatterOption, + ...options, + }; + + // + return clearFormat(options, clearLocale, value) || ''; + } + + // + private format(options: XFormatterOption, value: any) { + return applyFormat( + { ...this.defaultFormatterOption, ...options }, + value ? value.toString() : '' + ); + } +} diff --git a/projects/x-framework-core/src/lib/pipes/x-log.pipe.ts b/projects/x-framework-core/src/lib/pipes/x-log.pipe.ts new file mode 100644 index 0000000..4da7cd7 --- /dev/null +++ b/projects/x-framework-core/src/lib/pipes/x-log.pipe.ts @@ -0,0 +1,19 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'xLog', + pure: true, +}) +export class XLogPipe implements PipeTransform { + /** + * do a console log from a pipe + * + * @param value the specified value + * @param args it is optional and unused here + */ + transform(value: any, ...args: any[]): any { + // + console.log('XLogPipe: ', value); + return null; + } +} diff --git a/projects/x-framework-core/src/lib/pipes/x-safe-html.pipe.ts b/projects/x-framework-core/src/lib/pipes/x-safe-html.pipe.ts new file mode 100644 index 0000000..8dbf0cb --- /dev/null +++ b/projects/x-framework-core/src/lib/pipes/x-safe-html.pipe.ts @@ -0,0 +1,16 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; + +@Pipe({ + name: 'xSafeHTML', + pure: true, +}) +export class XSafeHtmlPipe implements PipeTransform { + // + constructor(private sanitizer: DomSanitizer) {} + + // + transform(value: any, args?: any): any { + return this.sanitizer.bypassSecurityTrustHtml(value); + } +} diff --git a/projects/x-framework-core/src/lib/tokens/x-injectable-tokens.ts b/projects/x-framework-core/src/lib/tokens/x-injectable-tokens.ts new file mode 100644 index 0000000..4ad0dd4 --- /dev/null +++ b/projects/x-framework-core/src/lib/tokens/x-injectable-tokens.ts @@ -0,0 +1,6 @@ +import { InjectionToken } from '@angular/core'; +import { XFrameworkCoreConfig } from '../config/x-framework-core.config'; + +export const X_FRAMEWORK_CORE_CONFIG = new InjectionToken( + 'x_config' +); diff --git a/projects/x-framework-core/src/lib/tools/x-array.tools.ts b/projects/x-framework-core/src/lib/tools/x-array.tools.ts new file mode 100644 index 0000000..de1d837 --- /dev/null +++ b/projects/x-framework-core/src/lib/tools/x-array.tools.ts @@ -0,0 +1,309 @@ +// +//#region Array Tools ... + +import { XOneOrManyType } from '../typings/x.typings'; +import { isArray } from './x-type-detect.tools'; + +/** + * retrieve last item of an Array ... + * + * @param list an instance of XOneOrManyType object ... + */ +export function lastItem(list: XOneOrManyType) { + // + if (!list || !hasChild(list)) { + return null; + } + + // + const listArray = toArray(list); + + // + return listArray[listArray.length - 1]; +} + +/** + * extract and retrieve specific item of an array ... + * + * @param index index of Array Item ... + * @param list an instance of XOneOrManyType object ... + */ +export function nthItem(index: number, list: XOneOrManyType) { + // + if (!list || !hasChild(list)) { + return null; + } + + // + const listArray = toArray(list); + if (index < 0) { + try { + return listArray[listArray.length - 1 + index]; + } catch { + return null; + } + } + + // + try { + return listArray[index]; + } catch { + return null; + } +} + +/** + * extract specific range of items from an array ... + * + * @param index extracting start index ... + * @param count number of extracting items ... + * @param list an instance of XOneOrManyType object ... + * @param reverse optional param to determines extracting direction ... + */ +export function nthItems( + index: number, + count: number, + list: XOneOrManyType, + reverse?: boolean +) { + // + if (!list || !hasChild(list) || count === 0) { + return null; + } + + // + let mIndex = index; + const listArray = toArray(list); + if (index < 0) { + // + mIndex = listArray.length - 1 + index; + } + + // + let mCount = count; + if (mCount < 0) { + // + mIndex += count; + mCount = count * -1; + } + + // + const result = listArray.splice(mIndex, mCount); + if (reverse) { + result.reverse(); + } + + // + return result; +} + +/** + * determines an array has childs or not ... + * + * @param list an instance of XOneOrManyType object ... + */ +export function hasChild(list: XOneOrManyType) { + return list && toArray(list).length > 0; +} + +/** + * count an array childs ... + * + * @param list an instance of XOneOrManyType object ... + */ +export function countChild(list: XOneOrManyType) { + return toArray(list).length; +} + +/** + * convert an indexable object to array ... + * + * @param index indexed object ... + */ +export function getArrayOf(index: { [key: string]: T }): T[] { + // + const result: T[] = []; + + // + Object.keys(index).forEach((key) => { + // + result.push(index[key] as T); + }); + + // + return result; +} + +/** + * merge all items inside collection of items ... + * @param args a collection of XOneOrManyOfType items ... + */ +export function mergeList(...args: XOneOrManyType[]) { + // + const result: T[] = []; + + // + args.forEach((arg) => { + result.push(...toArray(arg)); + }); + + // + return result; +} + +/** + * convert value to array ... + * + * @param list an instance of XOneOrManyType object ... + */ +export function toArray(list: XOneOrManyType) { + // + if (!list) { + return []; + } + + // + if (isArray(list)) { + return list as T[]; + } + + // + return [list as T]; +} + +/** + * convert a value of specific type to array of another type ... + * + * @param list an instance of XOneOrManyType object ... + * @returns an instance of TDest[] object ... + */ +export function convertArray( + list: XOneOrManyType +): XOneOrManyType { + // + let array = toArray(list); + + // + const result: TDest[] = !hasChild(array) + ? [] + : array.map((i) => i as any as TDest); + + // + return result; +} + +/** + * Calls a defined callback function on each element of an array, and returns an array that contains the results. + * + * @param list an instance of XOneOrManyType object ... + * @param callback A function that accepts up to three arguments. + * The map method calls the callbackfn function one time for each element in the array. + */ +export function mapArray( + list: XOneOrManyType, + callback: (value: T, index: number, array: T[]) => T +) { + // + if (!list) { + return null; + } + + // + const pList = toArray(list); + + // + return pList.map(callback); +} + +/** + * remove specific index of an array objects and return cleared array ... + * + * @param list an instance of XOneOrManyType object ... + * @param index specific item index ... + */ +export function removeItemByIndex(list: XOneOrManyType, index: number) { + // + if (!hasChild(list) || index < 0 || index > countChild(list) - 1) { + return []; + } + + // + return toArray(list).splice(index, 1); +} + +/** + * remove specific item of an array and return cleared array ... + * + * @param list an instance of XOneOrManyType object ... + * @param item specific item ... + */ +export function removeItem(list: XOneOrManyType, item: T) { + // + if (!hasChild(list) || !item) { + return []; + } + + // + const listArray = toArray(list); + const iIndex = listArray.indexOf(item); + if (iIndex === -1) { + return []; + } + + // + return listArray.splice(iIndex, 1); +} + +/** + * check an array contains an specific item or not ... + * + * @param list an instance of XOneOrManyType object ... + * @param item specific item ... + */ +export function isExists(list: XOneOrManyType, item: T) { + return toArray(list).findIndex((i) => i === item) !== -1; +} + +/** + * sort array ... + * + * @param list an instance of XOneOrManyType object ... + * @param compareFn campare function ... + */ +export function sort( + list: XOneOrManyType, + compareFn?: (a: T, b: T) => number +) { + // + const array = toArray(list); + var result = array.sort(compareFn); + + // + return result; +} + +/** + * retrieve only unique members of an Array ... + * + * @param list an instance of XOneOrManyType object ... + */ +export function onlyUniques(list: XOneOrManyType) { + return toArray(list).filter((v, i, s) => s.indexOf(v) === i); +} + +/** + * Check two Array is Equals or not ... + * + * @param a + * @param b + * @returns + */ +export function isXEqual(a: T[], b: T[]): boolean { + if (a.length !== b.length) return false; + return a.every( + (value, index) => + JSON.stringify(value) === JSON.stringify(b[index]) && + typeof value === typeof b[index] + ); +} +//#endregion diff --git a/projects/x-framework-core/src/lib/tools/x-color.tools.ts b/projects/x-framework-core/src/lib/tools/x-color.tools.ts new file mode 100644 index 0000000..dbd9ac9 --- /dev/null +++ b/projects/x-framework-core/src/lib/tools/x-color.tools.ts @@ -0,0 +1,1181 @@ +import { + isNumber, + isString, + isNullOrUndefined, + isNullOrEmptyString, +} from './x-type-detect.tools'; +import { + XColor, + XRGBAColor, + XHSVAColor, + XHSLAColor, + XColorIdentifier, + XColorDescribtor, + XColorBrightnessLevel, + XColorFormatIdentifier, +} from '../typings/x-color.typings'; +import { toNumber, toXString } from './x-type-convert.tools'; + +// +//#region XColor tools .. +/** + * retrieve a color name ... + * + * @param color the colorname + * @param level the color brightness which default is Regular + */ +export function getXColorName( + color: XColorIdentifier, + level: XColorBrightnessLevel = XColorBrightnessLevel.Regular +) { + // + const mColor = color.toString(); + return level === XColorBrightnessLevel.Regular + ? mColor + : mColor + `-${level.toString()}`; +} + +/** + * retrieve specific color CSS Variable Name ... + * + * @param color Color name + * @param isContrast determines to get Text Color on Color var name + * @param level determines color brightness level + */ +export function getXColorCssVariableName( + color: XColorIdentifier, + isContrast: boolean = false, + level: XColorBrightnessLevel = XColorBrightnessLevel.Regular +) { + // + return `--x-color-${color.toLowerCase()}${ + level === XColorBrightnessLevel.Regular ? '' : '-' + level.toLowerCase() + }${!!isContrast ? '-contrast' : ''}`; +} + +/** + * retrieve a color class \ name ... + * + * @param color the colorname + * @param level the color brightness which default is Regular + */ +export function getXColorClassName( + color: XColorIdentifier, + level: XColorBrightnessLevel = XColorBrightnessLevel.Regular +) { + // + if (color.toString() === 'inherit') { + return 'cb-inherit'; + } + + // + const mColor = 'mat-' + color.toString(); + return level === XColorBrightnessLevel.Regular + ? mColor + : mColor + `-${level.toString()}`; +} + +/** + * retrieve a color foreground class name ... + * + * @param color the colorname + * @param level the color brightness which default is Regular + */ +export function getXColorForegroundClassName(color: XColorIdentifier) { + // + if (color.toString() === 'inherit') { + return 'cb-inherit'; + } + + // + const mColor = 'c-' + color.toString(); + return mColor; +} + +/** + * parse a color identifier and return it's descriptor ... + * + * @param color the color identifier to parse ... + */ +export function parseXColor(color: XColorIdentifier): XColorDescribtor { + // + const strColor = color.toString().trim().toLowerCase(); + const isContrast = strColor.includes('contrast'); + const brightnessLevel: XColorBrightnessLevel = strColor.includes('tint') + ? XColorBrightnessLevel.Tint + : strColor.includes('shade') + ? XColorBrightnessLevel.Shade + : XColorBrightnessLevel.Regular; + const name = strColor + .replace('-', '') + .replace('contrast', '') + .replace(brightnessLevel.toString(), '') as XColor; + + // + const result: XColorDescribtor = { + name, + brightnessLevel, + isContrast, + }; + + // + return result; +} +//#endregion + +// +//#region Color Parsers / Converters ... +// +//#region RGBA Source ... +/** + * determine a color is XRGBAColor or not ... + * + * @param v an input color representation ... + */ +export function isRGB(v: any) { + // + if (isNullOrUndefined(v)) { + return false; + } + + // + const color = v as XRGBAColor; + if (isNullOrUndefined(color)) { + // + const result = + !isNullOrUndefined(v.r) && + isNumber(v.r) && + !isNullOrUndefined(v.g) && + isNumber(v.g) && + !isNullOrUndefined(v.b) && + isNumber(v.b); + // + return result; + } + + // + if ( + isString(v) && + toXString(v).includes('rgb') && + !toXString(v).includes('rgba') + ) { + return true; + } + + // + return false; +} + +/** + * determine a color is XRGBAColor or not ... + * + * @param v an input color representation ... + */ +export function isRGBA(v: any) { + // + if (isNullOrUndefined(v)) { + return false; + } + + // + const color = v as XRGBAColor; + if (isNullOrUndefined(color)) { + // + const result = + !isNullOrUndefined(v.r) && + isNumber(v.r) && + !isNullOrUndefined(v.g) && + isNumber(v.g) && + !isNullOrUndefined(v.b) && + isNumber(v.b) && + !isNullOrUndefined(v.a) && + isNumber(v.a); + + // + return result; + } + + // + if (isString(v) && toXString(v).includes('rgba')) { + return true; + } + + // + return false; +} + +/** + * convert an rgb color to hex representation ... + * + * @param color an instance of XRGBAColor ... + */ +export function rgbaToHex(color: XRGBAColor): string { + // + const r = color.r < 0 ? 0 : color.r > 255 ? 255 : color.r; + const g = color.g < 0 ? 0 : color.g > 255 ? 255 : color.g; + const b = color.b < 0 ? 0 : color.b > 255 ? 255 : color.b; + const a = color.a < 0 ? 0 : color.a > 1 ? 1 : color.a; + + // + let sr = r.toString(16); + let sg = g.toString(16); + let sb = b.toString(16); + let sa = Math.round(a * 255).toString(16); + + // + sr = sr.length === 1 ? '0' + sr : sr; + sg = sg.length === 1 ? '0' + sg : sg; + sb = sb.length === 1 ? '0' + sb : sb; + sa = sa.length === 1 ? '0' + sa : sa; + + // + const result = '#' + sr + sg + sb + sa; + + // + return result; +} + +/** + * convert an rgb color to hex representation ... + * + * @param color an instance of XRGBAColor ... + * @param allowHex8 determines hext string length be 8 char or not ... + */ +export function rgbaToHex1( + color: XRGBAColor, + allowHex8: boolean = true +): string { + // + let result = + '#' + + // tslint:disable-next-line:no-bitwise + ((1 << 24) | (color.r << 16) | (color.g << 8) | color.b) + .toString(16) + .substr(1); + + // + if ( + result[1] === result[2] && + result[3] === result[4] && + result[5] === result[6] && + color.a === 1 && + !allowHex8 + ) { + result = '#' + result[1] + result[3] + result[5]; + } + + // + if (allowHex8) { + // tslint:disable-next-line:no-bitwise + result += ((1 << 8) | Math.round(color.a * 255)).toString(16).substr(1); + } + + // + return result; +} + +/** + * get an rgb string form color representation ... + * + * @param color an instance of XRGBAColor ... + */ +export function rgbString(color: XRGBAColor): string { + // + if (isNullOrUndefined(color)) { + return 'rgba(0, 0, 0, 1)'; + } + + // + const r = color.r < 0 ? 0 : color.r > 255 ? 255 : color.r; + const g = color.g < 0 ? 0 : color.g > 255 ? 255 : color.g; + const b = color.b < 0 ? 0 : color.b > 255 ? 255 : color.b; + + // + const result = 'rgb(' + r + ',' + g + ',' + b + ')'; + + // + return result; +} + +/** + * get an rgba string form color representation ... + * + * @param color an instance of XRGBAColor ... + */ +export function rgbaString(color: XRGBAColor): string { + // + if (isNullOrUndefined(color)) { + return 'rgba(0, 0, 0, 1)'; + } + + // + const r = color.r < 0 ? 0 : color.r > 255 ? 255 : color.r; + const g = color.g < 0 ? 0 : color.g > 255 ? 255 : color.g; + const b = color.b < 0 ? 0 : color.b > 255 ? 255 : color.b; + const a = color.a < 0 ? 0 : color.a > 1 ? 1 : color.a; + + // + const result = 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'; + + // + return result; +} + +/** + * convert hsva to XHSVAColor ... + * + * @param rgba an XRGBAColor representation of color ... + */ +export function rgbaToHsva(rgba: XRGBAColor): XHSVAColor { + // + let result: XHSVAColor = null; + + // + // Validate Args ... + if (isNullOrUndefined(rgba)) { + return result; + } + + // + rgba.r /= 255; + rgba.g /= 255; + rgba.b /= 255; + + // + let a = Math.min(rgba.a, 1); + + // + let max = Math.max(rgba.r, rgba.g, rgba.b); + let min = Math.min(rgba.r, rgba.g, rgba.b); + + // + let h, + s, + v = max; + var d = max - min; + s = max == 0 ? 0 : d / max; + if (max == min) { + h = 0; // achromatic + } else { + switch (max) { + // + case rgba.r: + h = (rgba.g - rgba.b) / d + (rgba.g < rgba.b ? 6 : 0); + break; + + // + case rgba.g: + h = (rgba.b - rgba.r) / d + 2; + break; + + // + case rgba.b: + h = (rgba.r - rgba.g) / d + 4; + break; + } + + // + h /= 6; + } + + // + return { + h, + s, + v, + a + } +} + +/** + * converts an string to RGBA instance ... + * + * @param value input string value ... + * @returns an instance of XRGBAColor ... + */ +export function stringToRGBA(value: string): XRGBAColor { + // + // validate args ... + if (!isRGBA(value) || isNullOrEmptyString(value)) { + return null; + } + + // + // define default result ... + let result: XRGBAColor = null; + + // + // replace color token ... + value = value.includes('rgba') + ? value.replace('rgba', '') + : value.includes('rgb') + ? value.replace('rgb', '') + : value; + + // + // Replace paranteses ... + value = value.replace('(', '').replace(')', '').replace(';', ''); + + // + // Convert to array ... + const valuesArr = value.split(','); + if (valuesArr.length === 3) { + valuesArr.push('1'); + } + + // + // Prepare Materials ... + const r = toNumber(valuesArr[0]); + const g = toNumber(valuesArr[1]); + const b = toNumber(valuesArr[2]); + const a = toNumber(valuesArr[3]); + + // + // Create result ... + result = { + r, + g, + b, + a, + }; + + // + // return result ... + return result; +} + +/** + * denormalize XRGBAColor instance ... + * + * @param rgba an XRGBAColor representation of color ... + */ +export function denormalizeRGBA(color: XRGBAColor): XRGBAColor { + // + const result: XRGBAColor = { + r: Math.round(color.r * 255), + g: Math.round(color.g * 255), + b: Math.round(color.b * 255), + a: color.a, + }; + + // + return result; +} +//#endregion + +// +//#region HEX Source ... +/** + * convert hex to HSV ... + * + * @param color string representor ... + */ +export function hexToHsva(hex: string): XHSVAColor { + // + let result: XHSVAColor = null; + + // + // validate args ... + if (isNullOrEmptyString(hex) || !isHEX(hex)) { + return result; + } + + // + // normalize hex ... + hex = normalizeHexColor(hex, 8); + + // + // Convert hex to rgba ... + const rgba = hexToRgba(hex); + if (isNullOrUndefined(rgba)) { + return result; + } + + // + // Generate result ... + result = rgbaToHsva(rgba); + + // + // return result ... + return result; +} + +/** + * convert hex to rgba ... + * + * @param hex an string which represent color hex ... + */ +export function hexToRgba(hex: string): XRGBAColor { + // + let r = '0'; + let g = '0'; + let b = '0'; + let a: any = 1; + + // + if (hex.length === 7) { + // + r = '0x' + hex[1] + hex[1]; + g = '0x' + hex[2] + hex[2]; + b = '0x' + hex[3] + hex[3]; + a = '0x' + hex[4] + hex[4]; + } else if (hex.length === 9) { + // + r = '0x' + hex[1] + hex[2]; + g = '0x' + hex[3] + hex[4]; + b = '0x' + hex[5] + hex[6]; + a = '0x' + hex[7] + hex[8]; + } + + // + a = +(a / 255).toFixed(3); + + // + const result: XRGBAColor = { + r: +r, + g: +g, + b: +b, + a, + }; + + return result; +} + +/** + * + * @param hex an string ... + */ +export function normalizeHexColor(hex: string, forceLength: 6 | 8 = 8) { + // + const isValid = isHEX(hex); + if (!isValid) { + return ''; + } + + // + hex = hex.startsWith('#') ? hex.replace('#', '') : hex; + hex = hex.toLowerCase().trim(); + + // + let result = ''; + const hexLength = hex.length; + const hexArr = Array.from(hex); + + // + // 6 Length ... + if (forceLength === 6) { + // + if (hexLength === 6) { + result = hex; + } else if (hexLength === 3 || hexLength === 4) { + result = + hexArr[0] + hexArr[0] + hexArr[1] + hexArr[1] + hexArr[2] + hexArr[2]; + } + + // + return `#${result}`; + } + + // + // 8 Length ... + if (hexLength === 8) { + result = hex; + } else if (hexLength === 7) { + result = + hexArr[0] + + hexArr[1] + + hexArr[2] + + hexArr[3] + + hexArr[4] + + hexArr[5] + + 'f' + + hexArr[6]; + } else if (hexLength === 6) { + result = hex + 'ff'; + } else if (hexLength === 4) { + result = + hexArr[0] + + hexArr[0] + + hexArr[1] + + hexArr[1] + + hexArr[2] + + hexArr[2] + + hexArr[3] + + hexArr[3]; + } else if (hexLength === 3) { + result = + hexArr[0] + hexArr[0] + hexArr[1] + hexArr[1] + hexArr[2] + hexArr[2]; + } + + // + return `#${result}`; +} + +/** + * determines an string is hex color or not ... + * + * @param hex an string ... + * @returns boolean + */ +export function isHEX(hex: string) { + // + if (isNullOrEmptyString(hex) || isNullOrEmptyString(hex.trim())) { + return false; + } + + // + hex = hex.toLowerCase().trim().replace('#', ''); + + // + const availableLength = [3, 4, 6, 8]; + const availableDigits = [ + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + ]; + + // + let result = availableLength.findIndex((l) => hex.length === l) > -1; + if (!!result) { + result = Array.from(hex).every((char) => availableDigits.includes(char)); + } + + // + return result; +} +//#endregion + +// +//#region HSVA Source ... +/** + * determine a color is XRGBAColor or not ... + * + * @param v an input color representation ... + */ +export function isHSVA(v: any) { + // + if (isNullOrUndefined(v)) { + return false; + } + + // + const color = v as XHSVAColor; + if (isNullOrUndefined(color)) { + // + const result = + !isNullOrUndefined(v.h) && + isNumber(v.h) && + !isNullOrUndefined(v.s) && + isNumber(v.s) && + !isNullOrUndefined(v.v) && + isNumber(v.v) && + !isNullOrUndefined(v.a) && + isNumber(v.a); + + // + return result; + } + + // + return true; +} + +/** + * convert hsv to hex ... + * + * @param color an instance of XHSVColor ... + */ +export function hsvaToHex(color: XHSVAColor): string { + // + const i = Math.floor(color.h * 6); + const f = color.h * 6 - i; + const p = color.v * (1 - color.s); + const q = color.v * (1 - f * color.s); + const t = color.v * (1 - (1 - f) * color.s); + + // + let r = 0; + let g = 0; + let b = 0; + + // + switch (i % 6) { + case 0: + r = color.v; + g = t; + b = p; + break; + case 1: + r = q; + g = color.v; + b = p; + break; + case 2: + r = p; + g = color.v; + b = t; + break; + case 3: + r = p; + g = q; + b = color.v; + break; + case 4: + r = t; + g = p; + b = color.v; + break; + case 5: + r = color.v; + g = p; + b = q; + break; + } + + // + r = Math.floor(r * 255) + 256; + g = Math.floor(g * 255) + 256; + b = Math.floor(b * 255) + 256; + + // + const result = + '#' + + r.toString(16).slice(1) + + g.toString(16).slice(1) + + b.toString(16).slice(1); + + // + return result; +} + +/** + * convert hsva to XHSLAColor ... + * + * @param hsva an XHSVAColor representation of color ... + */ +export function hsvaToHsla(hsva: XHSVAColor): XHSLAColor { + // + let s = hsva.s; + const h = hsva.h; + const v = hsva.v; + const a = hsva.a; + + // + let result: XHSLAColor = {}; + + // + if (v === 0) { + // + result = { + h, + s: 0, + l: 0, + a, + }; + } else if (s === 0 && v === 1) { + // + result = { + h, + s: 1, + l: 1, + a, + }; + } else { + // + const l = (v * (2 - s)) / 2; + s = (v * s) / (1 - Math.abs(2 * l - 1)); + + // + result = { + h, + s, + l, + a, + }; + } + + // + return result; +} + +/** + * convert hsv to rgba ... + * + * @param color an instance of XHSVColor ... + */ +export function hsvaToRgba(hsva: XHSVAColor): XRGBAColor { + // + const h = hsva.h; + const s = hsva.s; + const v = hsva.v; + const a = hsva.a; + + // + let r: number; + let g: number; + let b: number; + + // + const i = Math.floor(h * 6); + const f = h * 6 - i; + const p = v * (1 - s); + const q = v * (1 - f * s); + const t = v * (1 - (1 - f) * s); + + // + switch (i % 6) { + case 0: + (r = v), (g = t), (b = p); + break; + case 1: + (r = q), (g = v), (b = p); + break; + case 2: + (r = p), (g = v), (b = t); + break; + case 3: + (r = p), (g = q), (b = v); + break; + case 4: + (r = t), (g = p), (b = v); + break; + case 5: + (r = v), (g = p), (b = q); + break; + } + + // + const result: XRGBAColor = { + r, + g, + b, + a, + }; + + // + return result; +} + +/** + * convert string representation of a color to XHSVAColor ... + * + * @param colorString an string representation of a color ... + */ +export function stringToHsva(colorString: string = ''): XHSVAColor { + // + // Validate Args ... + if (isNullOrEmptyString(colorString)) { + return null; + } + + // + // Result ... + let result: XHSVAColor = null; + + // + // Get Type ... + const type = isHEX(colorString) + ? 'hex' + : isRGBA(colorString) + ? 'rgba' + : isRGB(colorString) + ? 'rgb' + : isHSVA(colorString) + ? 'hsva' + : isHSLA(colorString) + ? 'hsla' + : 'unknown'; + + // + // get result value based on input type ... + switch (type) { + // + // 1 Hex ... + case 'hex': + result = hexToHsva(colorString); + break; + + // + // 2 RGB ... + // 3 RGBA ... + case 'rgb': + case 'rgba': + // + const rgba = stringToRGBA(colorString); + result = rgbaToHsva(rgba); + break; + + // + result = null; + break; + + // + // 5 HSLA ... + case 'hsla': + const hsla = stringToHsla(colorString); + result = hslaToHsva(hsla); + break; + + // + // 4 HSVA ... + // 6 Unknown ... + case 'hsva': + case 'unknown': + result = null; + break; + } + + // + // return result ... + return result; +} +//#endregion + +// +//#region HSLA Source ... +/** + * determine a color is XRGBAColor or not ... + * + * @param v an input color representation ... + */ +export function isHSLA(v: any) { + // + if (isNullOrUndefined(v)) { + return false; + } + + // + const color = v as XHSLAColor; + if (isNullOrUndefined(color)) { + // + const result = + !isNullOrUndefined(v.h) && + isNumber(v.h) && + !isNullOrUndefined(v.s) && + isNumber(v.s) && + !isNullOrUndefined(v.l) && + isNumber(v.l) && + !isNullOrUndefined(v.a) && + isNumber(v.a); + + // + return result; + } + + // + return true; +} + +/** + * convert hsla to XHSVAColor ... + * + * @param hsla an XHSLAColor representation of color ... + */ +export function hslaToHsva(hsla: XHSLAColor): XHSVAColor { + // + let s = Math.min(hsla.s, 1); + const h = Math.min(hsla.h, 1); + const l = Math.min(hsla.l, 1); + const a = Math.min(hsla.a, 1); + + // + let result: XHSVAColor = {}; + + // + if (l === 0) { + result = { + h, + s: 0, + v: 0, + a: 0, + }; + } else { + // + const v = l + (s * (1 - Math.abs(2 * l - 1))) / 2; + s = (2 * (v - l)) / v; + + // + result = { + h, + s, + v, + a, + }; + } + + // + return result; +} + +/** + * get an hsla string form color representation ... + * + * @param color an instance of XHSLAColor ... + */ +export function hslaString(color: XHSLAColor): string { + // + if (isNullOrUndefined(color)) { + return 'hsla(0, 100%, 100%, 1)'; + } + + // + const result = + 'hsla(' + color.h + ',' + color.s + '%,' + color.l + '%,' + color.a + ')'; + + // + return result; +} + +/** + * converts string to XHSLAColor ... + * + * @param value string value ... + * @returns instance of XHSLAColor instance ... + */ +export function stringToHsla(value: string): XHSLAColor { + // + let result: XHSLAColor = null; + + // + // Validate Args ... + if (isNullOrEmptyString(value) || !value.includes('hsla')) { + return result; + } + + // + // clear unused chars ... + const hslaArray = value + .replace('%', '') + .replace('(', '') + .replace(')', '') + .replace(';', '') + .split(','); + if (hslaArray.length < 4) { + return result; + } + + // + // generate required parts ... + const h = toNumber(hslaArray[0]); + const s = toNumber(hslaArray[1]); + const l = toNumber(hslaArray[2]); + const a = toNumber(hslaArray[3]); + + // + // Create result ... + result = { + h, + s, + l, + a, + }; + + // + // return result ... + return result; +} +//#endregion +//#endregion + +/** + * format input color ... + * + * @param hsva an XHSVAColor representation of a color ... + * @param format the type of output ... + * @param allowHex8 convert to 8 length string or not ... + */ +export function formatColor( + hsva: XHSVAColor, + format: XColorFormatIdentifier, + allowHex8: boolean +): string { + // + if (isNullOrUndefined(hsva)) { + hsva = { + h: 0, + s: 0, + v: 0, + a: 1, + }; + } + + // + if (hsva.a < 1) { + // + switch (format) { + // + case 'hsla': + // + const hsla = hsvaToHsla(hsva); + + // + const hslaColor: XHSLAColor = { + h: Math.round(hsla.h * 360), + s: Math.round(hsla.s * 100), + l: Math.round(hsla.l * 100), + a: Math.round(hsla.a * 100) / 100, + }; + + // + return hslaString(hslaColor); + + // + default: + // + if (allowHex8 && format === 'hex') { + return rgbaToHex1(denormalizeRGBA(hsvaToRgba(hsva)), allowHex8); + } + + // + const rgba = denormalizeRGBA(hsvaToRgba(hsva)); + rgba.a = Math.round(rgba.a * 100) / 100; + + // + return rgbaString(rgba); + } + } else { + // + switch (format) { + // + case 'hsla': + // + const hsla = hsvaToHsla(hsva); + + // + const hslaColor: XHSLAColor = { + h: Math.round(hsla.h * 360), + s: Math.round(hsla.s * 100), + l: Math.round(hsla.l * 100), + a: Math.round(hsla.a * 100) / 100, + }; + + // + return hslaString(hslaColor); + + // + case 'rgba': + // + const rgbaColor = denormalizeRGBA(hsvaToRgba(hsva)); + + // + return rgbaString(rgbaColor); + + // + default: + return rgbaToHex1(denormalizeRGBA(hsvaToRgba(hsva)), allowHex8); + } + } +} diff --git a/projects/x-framework-core/src/lib/tools/x-date-time.tools.ts b/projects/x-framework-core/src/lib/tools/x-date-time.tools.ts new file mode 100644 index 0000000..b592f23 --- /dev/null +++ b/projects/x-framework-core/src/lib/tools/x-date-time.tools.ts @@ -0,0 +1,574 @@ +import { + isDate, + isString, + isNumber, + isNullOrUndefined, + isNullOrEmptyString, +} from './x-type-detect.tools'; +import * as moment_ from 'jalali-moment'; +const moment = moment_; +import { Moment, unitOfTime } from 'jalali-moment'; +import { toXString } from './x-type-convert.tools'; +import { toPersianNumbers } from './x-string.tools'; +import { XLocale } from '../typings/x-translation.typings'; +import { XUnitOfTime } from '../typings/x-date-time.typings'; + +// +//#region Date Time Utils ... +export function isDefaultUTCDate(utcDate: Date | string | number): boolean { + // + var dateString = isNullOrUndefined(utcDate) + ? '0001-01-01T00:00:00' + : toXString(utcDate); + if ( + dateString === '0001-01-01T00:00:00' || + dateString === '1901-01-01T00:00:00.000Z' + ) { + return true; + } + + // + const mDateString = fromUtcDate(utcDate).toISOString(); + if ( + mDateString === '0001-01-01T00:00:00' || + mDateString === '1901-01-01T00:00:00.000Z' + ) { + return true; + } + + // + return false; +} + +/** + * convert a UTC date to locale ... + */ +export function fromUtcDate(utcDate: Date | string | number): Date { + // + const mUtcDate = new Date(utcDate); + + // + const utcDateObject = Date.UTC( + mUtcDate.getFullYear(), + mUtcDate.getMonth(), + mUtcDate.getDate(), + mUtcDate.getHours(), + mUtcDate.getMinutes(), + mUtcDate.getSeconds(), + mUtcDate.getMilliseconds() + ); + + // + return new Date(utcDateObject); +} + +export function fromUtcString(utcDateString: string): Date { + // + const utcDate = new Date(utcDateString); + + // + return fromUtcDate(utcDate); +} + +export function fromUtc(date: Date | string) { + return isDate(date) + ? fromUtcDate(date) + : isString(date) + ? fromUtcString(date.toString()) + : new Date(); +} + +/** + * format a Date to specific format + * and return the formatted string, based on forLocal ... + */ +export function formatDate( + date: Date | string, + forLocale: XLocale, + format: string, + isUtc: boolean = false, + toUtc: boolean = false +) { + // + let momentDate = isUtc ? moment(fromUtc(date)) : moment(date); + + // + if (toUtc) { + momentDate = momentDate.utc(); + } + + // + let result = momentDate.format(format); + + // + if (forLocale === 'fa-IR') { + result = toPersianNumbers(result); + } + + // + return result; +} + +export function getMomentInstance( + input?: string | number | Date, + isUtc?: boolean +): Moment { + // + let startDate = moment(input); + + // + if (isUtc) { + startDate = moment(input).utc(); + } + + // + return startDate; +} + +export function getDateFormatString(locale?: XLocale) { + // + if (locale === 'fa-IR') { + return 'HH:mm:ss jYYYY/jMM/jDD'; + } + + // + return 'YYYY-MM-DD HH:mm:ss'; +} + +export function getDateOfBirthDateFormatString(locale?: XLocale) { + // + if (locale === 'fa-IR') { + return 'jYYYY/jMM/jDD'; + } + + // + return 'YYYY/MM/DD'; +} + +export function getErrorDateFormatString(locale?: XLocale) { + // + if (locale === 'fa-IR') { + return 'HH:mm:ss-jYYYY/jMM/jDD'; + } + + // + return 'YYYY-MM-DD HH:mm:ss'; +} + +export function getRegularDateFormatString(locale?: XLocale) { + // + if (locale === 'fa-IR') { + return 'HH:mm:ss jYYYY-jMM-jDD'; + } + + // + return 'YYYY-MM-DD HH:mm:ss'; +} + +export function getJustYearFormatString(locale?: XLocale) { + // + if (locale === 'fa-IR') { + return 'jYYYY'; + } + + // + return 'YYYY'; +} + +export function getYear(momentDate: Moment) { + return momentDate.format('YYYY'); +} + +export function getMonth(momentDate: Moment) { + return momentDate.format('MM'); +} + +export function getDay(momentDate: Moment) { + return momentDate.format('DD'); +} + +export function getHours(momentDate: Moment) { + return momentDate.format('HH'); +} + +export function getMinutes(momentDate: Moment) { + return momentDate.format('mm'); +} + +export function getSeconds(momentDate: Moment) { + return momentDate.format('ss'); +} + +export function getMilliseconds(momentDate: Moment) { + return momentDate.format('SSS'); +} + +export function getUTCDate(date?: any) { + // + if (!date) { + date = new Date(); + } + + // + const mDate = new Date(date).toUTCString(); + + // + return new Date(mDate); +} + +export function getUTCDateString(date?: any) { + // + if (!date) { + date = new Date(); + } + + // + const mDate = new Date(date).toUTCString(); + + // + return mDate; +} + +export function createDate( + year?: string, + month?: string, + day?: string, + hours?: string, + minutes?: string, + seconds?: string, + milliseconds?: string +) { + // + const result = new Date(); + + // + if (year) { + result.setFullYear(+year); + } + + // + if (month) { + result.setMonth(+month - 1); + } + + // + if (day) { + result.setDate(+day); + } + + // + if (hours) { + result.setHours(+hours); + } + + // + if (minutes) { + result.setMinutes(+minutes); + } + + // + if (seconds) { + result.setSeconds(+seconds); + } + + // + if (milliseconds) { + result.setMilliseconds(+milliseconds); + } + + // + return result; +} + +export function getUnitOfTimeForLocale( + unit: XUnitOfTime, + locale: XLocale = 'en-US' +) { + // + let result: unitOfTime.DurationConstructor = null; + + // + switch (unit) { + // + case XUnitOfTime.Year: + // + switch (locale) { + // + case 'en-US': + result = 'year'; + break; + + // + case 'fa-IR': + result = 'jYear'; + break; + } + break; + + // + case XUnitOfTime.Month: + // + switch (locale) { + // + case 'en-US': + result = 'month'; + break; + + // + case 'fa-IR': + result = 'jMonth'; + break; + } + break; + + // + case XUnitOfTime.Day: + // + switch (locale) { + // + case 'en-US': + result = 'day'; + break; + + // + case 'fa-IR': + result = 'jDay'; + break; + } + break; + } + + // + return result; +} + +/** + * add specific amount of year to start date ... + * + * @param date start date ... + * @param value amount of specific unit of time ... + * @param locale the locale which required to date handling ... + */ +export function addYear( + date: string | number | Date, + value: number, + locale: XLocale = 'en-US' +) { + // + if (isNullOrUndefined(date) || isNullOrEmptyString(locale)) { + return getMomentInstance(date); + } + + // + let yearSpecifier = getUnitOfTimeForLocale(XUnitOfTime.Year, locale); + if (isNullOrUndefined(yearSpecifier)) { + yearSpecifier = 'year'; + } + + // + return getMomentInstance(date).add(value, yearSpecifier).toDate(); +} + +/** + * add specific amount of month to start date ... + * + * @param date start date ... + * @param value amount of specific unit of time ... + * @param locale the locale which required to date handling ... + */ +export function addMonth( + date: string | number | Date, + value: number, + locale: XLocale = 'en-US' +) { + // + if (isNullOrUndefined(date) || isNullOrEmptyString(locale)) { + return getMomentInstance(date); + } + + // + let yearSpecifier = getUnitOfTimeForLocale(XUnitOfTime.Month, locale); + if (isNullOrUndefined(yearSpecifier)) { + yearSpecifier = 'month'; + } + + // + return getMomentInstance(date).add(value, yearSpecifier).toDate(); +} + +/** + * add specific amount of days to start date ... + * + * @param date start date ... + * @param value amount of specific unit of time ... + * @param locale the locale which required to date handling ... + */ +export function addDay( + date: string | number | Date, + value: number, + locale: XLocale = 'en-US' +) { + // + if (isNullOrUndefined(date) || isNullOrEmptyString(locale)) { + return getMomentInstance(date); + } + + // + let yearSpecifier = getUnitOfTimeForLocale(XUnitOfTime.Day, locale); + if (isNullOrUndefined(yearSpecifier)) { + yearSpecifier = 'day'; + } + + // + return getMomentInstance(date).add(value, yearSpecifier).toDate(); +} + +export function addUnitOfTime( + unit?: XUnitOfTime, + value?: number, + date?: string | number | Date, + locale?: XLocale +) { + // + if (isNullOrUndefined(unit)) { + unit = XUnitOfTime.Year; + } + + // + if (isNullOrUndefined(date)) { + date = new Date(); + } + + // + if (isNullOrUndefined(locale)) { + locale = 'en-US'; + } + + // + if (isNullOrUndefined(value)) { + value = 0; + } + + // + return getMomentInstance(date) + .add(value, getUnitOfTimeForLocale(unit, locale)) + .toDate(); +} + +/** + * get date time format string of given value ... + */ +export function toTimeFormat(value: number) { + // + if (value < 10) { + return `0${value}`; + } else { + return `${value}`; + } +} + +export function addSecconds(value?: Date, secconds?: number) { + // + let result = new Date(); + + // + if (value) { + result = value; + } + + // + if (secconds) { + result = new Date(result.getTime() + secconds * 1000); + } + + // + return result; +} + +export function addMinutes(value?: Date, minutes?: number) { + return addSecconds(value, minutes ? minutes * 60 : minutes); +} + +export function addHours(value?: Date, hours?: number) { + return addMinutes(value, hours ? hours * 60 : hours); +} + +export function isExpiredDate(date: Date | string | number) { + // + const nowMills = new Date().getTime(); + const mDateTimeMills = new Date(date).getTime(); + + // + return nowMills > mDateTimeMills; +} + +export function isExpiredUTCDate(utcDate: Date | string | number) { + // + const nowUTCString = new Date().toUTCString(); + const nowMills = fromUtcDate(nowUTCString).getTime(); + const mUtcDateTimeMills = fromUtcDate(utcDate).getTime(); + + // + return nowMills > mUtcDateTimeMills; +} + +/** + * convert a date to it's milliseconds representation format ... + * + * @param date date representer + */ +export function getMills(date?: Date | any) { + // + let mDate: Date; + if (!date) { + mDate = new Date(); + } else { + mDate = new Date(date); + } + + // + return mDate.getTime(); +} + +/** + * convert any object to date ... + */ +export function momentToDate(date: any) { + return new Date(moment(date.toString()).format()); +} + +/** + * check and compare two date must be same ... + */ +export function isSameDate(sourceDate: any, destDate: any) { + // + const sourceTime = new Date(sourceDate).getTime(); + const destTime = new Date(destDate).getTime(); + + // + return sourceDate === destDate; +} + +/** + * check a given value has a date or not ... + * + * @param source given value to check ... + */ +export function isValueDate(source: any) { + // + let result = isDate(source); + if (!result && (isString(source) || isNumber(source))) { + // + let d: Date = null; + try { + d = new Date(source); + } catch {} + + // + result = !isNullOrUndefined(d) && isDate(d); + } + + // + return result; +} +//#endregion diff --git a/projects/x-framework-core/src/lib/tools/x-exception.tools.ts b/projects/x-framework-core/src/lib/tools/x-exception.tools.ts new file mode 100644 index 0000000..6b3fa42 --- /dev/null +++ b/projects/x-framework-core/src/lib/tools/x-exception.tools.ts @@ -0,0 +1,145 @@ +import { + keys, + getVal, + fromJson, + parseJson, + toJsonString, +} from './x-object.tools'; +import { toNumber } from './x-type-convert.tools'; +import { toNormalString } from './x-string.tools'; +import { HttpErrorResponse } from '@angular/common/http'; +import { XExceptions, XException } from '../typings/x-exception.typings'; +import { isNumber, isString, isNullOrUndefined } from './x-type-detect.tools'; +import { XExceptionIDs, Exceptions } from '../constants/x-exception.resources'; + +export function getXException(exception: XExceptionIDs | XExceptions) { + // + const isExId = isNumber(exception); + const exceptionKey = isExId ? getVal(XExceptionIDs, exception) : exception; + + // + return Exceptions[exceptionKey]; +} + +export function getXExceptionsArray() { + // + const result: XException[] = + keys(Exceptions).map((key: any) => Exceptions[key]) || []; + + // + return result; +} + +export function getXExceptionByErrorId(errorId: number) { + // + const result = getXExceptionsArray().find((e) => e.id === toNumber(errorId)); + return result; +} + +/** + * Prepare and Return Specific Error object + */ +export function getError(error: XExceptionIDs | XExceptions | XException) { + // + const isExId = isNumber(error); + const isExTitle = isString(error); + + // + return new Error( + toJsonString( + isExId + ? getXException(error as XExceptionIDs) + : isExTitle + ? getXException(error as XExceptions) + : (error as XException) + ) + ); +} + +/** + * extract Exception information of given error param + */ +export function getErrorException(err: any = undefined) { + // + if (isNullOrUndefined(err)) { + err = Exceptions.Unknown; + } + + // + let exception: XException; + + // + if (err instanceof HttpErrorResponse) { + err = err.error; + } else if (err instanceof Error) { + err = parseJson(err.message); + } else { + try { + err = parseJson(toNormalString(err)); + } catch {} + } + + // + if (isValidException(err)) { + exception = getXExceptionByErrorId(err.id); + } else if (isNumber(err) || isString(err)) { + exception = getXException(err); + } else if (err.id && isNumber(err.id)) { + exception = getXExceptionByErrorId(err.id); + } + + // + if (!exception) { + exception = Exceptions.Unknown; + } + + // + return exception; +} + +/** + * check an object is Valid Exception or not ... + */ +export function isValidException(err: any) { + // + if (isNullOrUndefined(err)) { + return false; + } + + // + if (err.id && isNumber(err.id) && err.message && isString(err.message)) { + return true; + } + + // + if (err.hasOwnProperty('id') && err.hasOwnProperty('message')) { + try { + const jExp = fromJson(err); + if (jExp.id && isNumber(jExp) && jExp.message && isString(jExp.message)) { + return true; + } + } catch {} + } + + // + const jsonStr = toNormalString(toJsonString(err)); + if (jsonStr.indexOf('id') !== -1 && jsonStr.indexOf('message') !== -1) { + return true; + } + + // + return false; +} + +export function throwException( + exception: XExceptionIDs | XExceptions | XException +): never { + // + throw getError(exception); +} + +export type XExceptionIdentifier = + | XExceptionIDs + | XExceptions + | XException + | string; diff --git a/projects/x-framework-core/src/lib/tools/x-file.tools.ts b/projects/x-framework-core/src/lib/tools/x-file.tools.ts new file mode 100644 index 0000000..8d71189 --- /dev/null +++ b/projects/x-framework-core/src/lib/tools/x-file.tools.ts @@ -0,0 +1,1232 @@ +import { + XMimeType, + XRouterTarget, + XMimeTypeIdentifier, + XRouterTargetIdentifier, +} from '../typings/x.typings'; +import { + isString, + isNullOrUndefined, + isNullOrEmptyString, +} from './x-type-detect.tools'; +import { + XFileType, + XFileInfos, + TypesMatcher, + XGeneralThumbnails, +} from '../constants/x-file.enum'; +import { hasChild } from './x-array.tools'; +import { generateUuid } from './x-uuid-tools'; +import { isSameObject } from './x-object.tools'; +import { toNormalString } from './x-string.tools'; +import { toXString } from './x-type-convert.tools'; +import { DomSanitizer } from '@angular/platform-browser'; +import { XResourceIDs } from '../constants/x-translation.resources'; + +// +//#region Definitions ... +export interface XFileBlob { + lastModified: number; + name: string; + size: number; + type: string; +} + +const defaultXFileBlob: XFileBlob = { + lastModified: 0, + name: '', + size: 0, + type: '', +}; +//#endregion + +// +//#region Helper Methdos ... +/** + * extract a file's extension ... + * + * @param fileName the file which going to get it's extension + */ +export function getExtension(fileName: string) { + // + const index = fileName.lastIndexOf('.'); + if (index === -1) { + return ''; + } + + // + return fileName.substr(index, fileName.length - index); +} + +/** + * find a file representer object ... + * + * @param path file path ... + * @returns an info object which represent informations about file ... + */ +export function getFileInfo(path: string) { + // + if (isNullOrEmptyString(path)) { + return null; + } + + // + const ext = getExtension(path); + if (isNullOrEmptyString(ext)) { + return null; + } + + // + const infoObj = XFileInfos.find( + (inf) => toNormalString(inf.extension) === toNormalString(ext) + ); + if (isNullOrUndefined(infoObj)) { + return null; + } + + // + return infoObj; +} + +/** + * determines an object instance is XFileBlob instance or not ... + * + * @param value any object instance ... + * @returns a boolean value ... + */ +export function isXFileBlob(value: any) { + return isSameObject(value, defaultXFileBlob); +} + +/** + * determines an extension's which exists or not in list + * + * @param allowedExtensions the extensions list + * @param fileExtension the file extensons which searched in list + */ +export function isExistsExtension( + allowedExtensions: string[], + fileExtension: string +) { + // + if (!fileExtension || !hasChild(allowedExtensions)) { + return false; + } + + // + let result = false; + allowedExtensions.forEach((ext) => { + // + if (ext.trim().toLowerCase() === fileExtension.trim().toLowerCase()) { + // + result = true; + } + }); + + // + return result; +} + +/** + * check a file extension is valid and exists in given collection ... + * @param file input source + * @param allowedExtensions allowed file extensions ... + */ +export function isExtensionOk(file: File, allowedExtensions: string[]) { + // + if (!file || !hasChild(allowedExtensions)) { + return false; + } + + // + const fileExt = getExtension(file.name); + const result = isExistsExtension(allowedExtensions, fileExt); + + // + return result; +} + +/** + * Determine the File Size Limitation Passed ... + * @param file File instance ... + */ +export function isSizeOk(file: File, maxSingleFileSize: number) { + return file.size <= maxSingleFileSize; +} + +/** + * Determine the File Size Limitation Passed ... + * @param file File instance ... + */ +export function isSizeOkForThumb(file: File, maxSingleFileThumbSize: number) { + return file.size <= maxSingleFileThumbSize; +} + +/** + * Determine the File is Image or not ... + * @param file File instance ... + */ +export function isImage(file: File | Blob | XFileBlob | string) { + // + if (file instanceof File || (file instanceof Blob && file.type)) { + return file.type.toLowerCase().indexOf(TypesMatcher.IMAGE) !== -1; + } else if (isXFileBlob(file)) { + // + const blob = file as XFileBlob; + const path = !isNullOrUndefined(blob) ? blob.name : ''; + const obj = getFileInfo(path); + if (isNullOrUndefined(obj)) { + return false; + } + + // + const result = + obj.type === XFileType.Image || + obj.type === XFileType.CoverImage || + obj.type === XFileType.ProfileImage; + return result; + } else if (isString(file)) { + // + const path = toXString(file); + const obj = getFileInfo(path); + if (isNullOrUndefined(obj)) { + return false; + } + + // + const result = + obj.type === XFileType.Image || + obj.type === XFileType.CoverImage || + obj.type === XFileType.ProfileImage; + return result; + } + + // + return false; +} + +/** + * Determine the File is Video or not ... + * @param file File instance ... + */ +export function isVideo(file: File | Blob | XFileBlob | string) { + // + if (file instanceof File || (file instanceof Blob && file.type)) { + return file.type.toLowerCase().indexOf(TypesMatcher.VIDEO) !== -1; + } else if (isXFileBlob(file)) { + // + const blob = file as XFileBlob; + const path = !isNullOrUndefined(blob) ? blob.name : ''; + const obj = getFileInfo(path); + if (isNullOrUndefined(obj)) { + return false; + } + + // + return obj.type === XFileType.Video; + } else if (isString(file)) { + // + const path = toXString(file); + const obj = getFileInfo(path); + if (isNullOrUndefined(obj)) { + return false; + } + + // + return obj.type === XFileType.Video; + } + + // + return false; +} + +/** + * Determine the File is Audio or not ... + * @param file File instance ... + */ +export function isAudio(file: File | Blob | XFileBlob | string) { + // + if (file instanceof File || (file instanceof Blob && file.type)) { + return file.type.toLowerCase().indexOf(TypesMatcher.AUDIO) !== -1; + } else if (isXFileBlob(file)) { + // + const blob = file as XFileBlob; + const path = !isNullOrUndefined(blob) ? blob.name : ''; + const obj = getFileInfo(path); + if (isNullOrUndefined(obj)) { + return false; + } + + // + return obj.type === XFileType.Video; + } else if (isString(file)) { + // + const path = toXString(file); + const obj = getFileInfo(path); + if (isNullOrUndefined(obj)) { + return false; + } + + // + return obj.type === XFileType.Video; + } + + // + return false; +} + +/** + * Determine the File is Document or not ... + * @param file File instance ... + */ +export function isDocument(file: File | Blob | XFileBlob | string) { + // + if (file instanceof File || (file instanceof Blob && file.type)) { + return !isImage(file) && !isAudio(file) && !isVideo(file); + } else if (isXFileBlob(file)) { + // + const blob = file as XFileBlob; + const path = !isNullOrUndefined(blob) ? blob.name : ''; + const obj = getFileInfo(path); + if (isNullOrUndefined(obj)) { + return false; + } + + // + return obj.type === XFileType.Document; + } else if (isString(file)) { + // + const path = toXString(file); + const obj = getFileInfo(path); + if (isNullOrUndefined(obj)) { + return false; + } + + // + return obj.type === XFileType.Document; + } + + // + return false; +} + +/** + * Check file size is ok and type is Image ... + * @param file File instance ... + */ +export function isImageAndSizeOk(file: File, maxSingleFileSize: number) { + return isSizeOk(file, maxSingleFileSize) && isImage(file); +} + +/** + * Check file size is ok and type is Audio ... + * @param file File instance ... + */ +export function isAudioAndSizeOk(file: File, maxSingleFileSize: number) { + return isSizeOk(file, maxSingleFileSize) && isAudio(file); +} + +/** + * Check file size is ok and type is Video ... + * @param file File instance ... + */ +export function isVideoAndSizeOk(file: File, maxSingleFileSize: number) { + return isSizeOk(file, maxSingleFileSize) && isVideo(file); +} + +/** + * Check file size is ok and type is Document ... + * @param file File instance ... + */ +export function isDocumentAndSizeOk(file: File, maxSingleFileSize: number) { + return isSizeOk(file, maxSingleFileSize) && isDocument(file); +} + +/** + * Check file size is ok and type is Image ... + * @param file File instance ... + */ +export function isImageAndSizeOkForThumb( + file: File, + maxSingleFileThumbSize: number +) { + return isSizeOkForThumb(file, maxSingleFileThumbSize) && isImage(file); +} + +/** + * Check file size is ok and type is Audio ... + * @param file File instance ... + */ +export function isAudioAndSizeOkForThumb( + file: File, + maxSingleFileThumbSize: number +) { + return isSizeOkForThumb(file, maxSingleFileThumbSize) && isAudio(file); +} + +/** + * Check file size is ok and type is Video ... + * @param file File instance ... + */ +export function isVideoAndSizeOkForThumb( + file: File, + maxSingleFileThumbSize: number +) { + return isSizeOkForThumb(file, maxSingleFileThumbSize) && isVideo(file); +} + +/** + * Check file size is ok and type is Document ... + * @param file File instance ... + */ +export function isDocumentAndSizeOkForThumb( + file: File, + maxSingleFileThumbSize: number +) { + return isSizeOkForThumb(file, maxSingleFileThumbSize) && isDocument(file); +} + +/** + * retrieve type of a file identifier ... + * + * @param file an identifier to a file ... + * @returns XFileType ... + */ +export function getFileType(file: File | Blob | XFileBlob | string | any) { + // + const isFile = file instanceof File || file instanceof Blob; + if (isFile) { + // + const mFile = file as Blob; + if (isImage(mFile)) { + return XFileType.Image; + } else if (isAudio(mFile)) { + return XFileType.Audio; + } else if (isVideo(mFile)) { + return XFileType.Video; + } else { + return XFileType.Document; + } + } else if ((file as any).type) { + return (file as any).type as XFileType; + } else if (isXFileBlob(file)) { + // + const obj = file as XFileBlob; + const path = obj.name; + const info = getFileInfo(path); + if (isNullOrUndefined(info)) { + return null; + } + + // + return info.type; + } else if (isString(file)) { + // + const path = toXString(file); + const info = getFileInfo(path); + if (isNullOrUndefined(info)) { + return null; + } + + // + return info.type; + } else { + return null; + } +} + +export function generateFileName(blob: Blob, prefix?: string) { + // + const type = getFileType(blob); + const result = `${ + isNullOrEmptyString(prefix) ? '' : prefix + '_' + }${generateUuid()}.${ + type === XFileType.Image + ? 'jpg' + : type === XFileType.Audio + ? 'mp3' + : type === XFileType.Video + ? 'mp4' + : 'doc' + }`; + + // + return result; +} + +export function getGeneralThumbnail( + file: File | Blob | XFileBlob | string | any +) { + // + let result = XGeneralThumbnails.UNKNOWN; + const type = getFileType(file); + + // + switch (type) { + // + case XFileType.Audio: + result = XGeneralThumbnails.AUDIO; + break; + + // + case XFileType.Video: + result = XGeneralThumbnails.VIDEO; + break; + case XFileType.Image: + result = XGeneralThumbnails.IMAGE; + break; + + // + case XFileType.Document: + default: + result = XGeneralThumbnails.DOCUMENT; + break; + } + + // + return result; +} + +/** + * represent File instance as FileBlob ... + * @param file the File instance + */ +export function toFileBlob(file: File): XFileBlob { + return { + lastModified: file.lastModified, + type: file.type, + name: file.name, + size: file.size, + } as XFileBlob; +} + +/** + * represent File instance as JSON String ... + * @param file the File instance + */ +export function fileToJsonString(file: File): string { + return JSON.stringify(toFileBlob(file)); +} + +/** + * parse FileBlob from a JSON String ... + * @param file the File instance + */ +export function fileFromJsonString(value: string): XFileBlob | null { + try { + return JSON.parse(value) as XFileBlob; + } catch { + return null; + } +} + +export function toFileBlobs(files: Set): XFileBlob[] { + // + const result: XFileBlob[] = []; + + // + files.forEach((f) => { + result.push(toFileBlob(f)); + }); + + // + return result; +} + +export function dataURLtoFile(dataurl: string | any, filename: string) { + // + const arr = dataurl.split(','); + const mime = arr[0].match(/:(.*?);/)[1]; + const bstr = atob(arr[1]); + let n = bstr.length; + const u8arr = new Uint8Array(n); + + while (n--) { + u8arr[n] = bstr.charCodeAt(n); + } + + // + return new File([u8arr], filename, { type: mime }); +} + +export function blobToFile(theBlob: Blob, fileName: string): File { + // + const b: any = theBlob; + + // + b.lastModifiedDate = new Date(); + b.name = fileName; + + // + return theBlob as File; +} + +export function getFileTypeResourceId(type: XFileType) { + switch (type) { + // + case XFileType.Audio: + return XResourceIDs.audio; + + // + case XFileType.Video: + return XResourceIDs.video; + + // + case XFileType.Document: + return XResourceIDs.document; + + // + case XFileType.ProfileImage: + return XResourceIDs.avatar; + + // + case XFileType.CoverImage: + return XResourceIDs.cover_image; + + // + case XFileType.Image: + return XResourceIDs.image; + + // + default: + return XResourceIDs.unknown; + } +} + +/** + * return file width and height ... + * + * @param file source image file + */ +export async function getImageDimensions(file: File) { + // + if (!file || !isImage(file)) { + throw new Error('Invalid Args ...'); + } + + // + return new Promise<{ width: number; height: number }>((res, err) => { + // + const reader: FileReader = new FileReader(); + + // + reader.onloadend = (e: any) => { + // + const dataUrl = e.target.result as string; + + // + const img = new Image(); + img.onload = () => { + // + res({ + width: img.width, + height: img.height, + }); + }; + + // + img.src = dataUrl; + }; + + // + reader.onerror = () => { + err('Error on Reading Image'); + }; + + // + reader.readAsDataURL(file); + }); +} + +export function isImageType(type: XFileType) { + switch (type) { + // + case XFileType.Image: + case XFileType.CoverImage: + case XFileType.ProfileImage: + return true; + + // + default: + return false; + } +} + +// +//#region Canvas & Image & ArrayBuffer & Blob & File Manipulation Tools ... +export function resetExifOrientation(srcBase64: string): Promise { + try { + const exifRotation = getExifRotation(srcBase64); + if (exifRotation > 1) { + return transformBase64BasedOnExifRotation(srcBase64, exifRotation); + } else { + return Promise.resolve(srcBase64); + } + } catch (ex) { + return Promise.reject(ex); + } +} + +export function transformBase64BasedOnExifRotation( + srcBase64: string, + exifRotation: number +): Promise { + return new Promise((resolve, reject) => { + const img = new Image(); + img.onload = () => { + const width = img.width; + const height = img.height; + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + if (ctx) { + if (4 < exifRotation && exifRotation < 9) { + canvas.width = height; + canvas.height = width; + } else { + canvas.width = width; + canvas.height = height; + } + transformCanvas(ctx, exifRotation, width, height); + ctx.drawImage(img, 0, 0); + resolve(canvas.toDataURL()); + } else { + reject(new Error('No context')); + } + }; + img.src = srcBase64; + }); +} + +export function resizeCanvas( + canvas: HTMLCanvasElement, + width: number, + height: number, + // tslint:disable-next-line:no-shadowed-variable + resizeCanvas: boolean = true +) { + const widthSource = canvas.width; + const heightSource = canvas.height; + width = Math.round(width); + height = Math.round(height); + + const ratioW = widthSource / width; + const ratioH = heightSource / height; + const ratioWHalf = Math.ceil(ratioW / 2); + const ratioHHalf = Math.ceil(ratioH / 2); + + const ctx = canvas.getContext('2d'); + if (ctx) { + const img = ctx.getImageData(0, 0, widthSource, heightSource); + const img2 = ctx.createImageData(width, height); + const data = img.data; + const data2 = img2.data; + + for (let j = 0; j < height; j++) { + for (let i = 0; i < width; i++) { + // + const x2 = (i + j * width) * 4; + const centerY = j * ratioH; + let weight = 0; + let weights = 0; + let weightsAlpha = 0; + let gxR = 0; + let gxG = 0; + let gxB = 0; + let gxA = 0; + + // + const xxStart = Math.floor(i * ratioW); + const yyStart = Math.floor(j * ratioH); + + // + let xxStop = Math.ceil((i + 1) * ratioW); + let yyStop = Math.ceil((j + 1) * ratioH); + xxStop = Math.min(xxStop, widthSource); + yyStop = Math.min(yyStop, heightSource); + + // + for (let yy = yyStart; yy < yyStop; yy++) { + // + const dy = Math.abs(centerY - yy) / ratioHHalf; + const centerX = i * ratioW; + const w0 = dy * dy; // pre-calc part of w + + // + for (let xx = xxStart; xx < xxStop; xx++) { + // + const dx = Math.abs(centerX - xx) / ratioWHalf; + const w = Math.sqrt(w0 + dx * dx); + + // + if (w >= 1) { + // pixel too far + continue; + } + + // + // hermite filter + weight = 2 * w * w * w - 3 * w * w + 1; + const posX = 4 * (xx + yy * widthSource); + + // + // alpha + gxA += weight * data[posX + 3]; + weightsAlpha += weight; + + // + // colors + if (data[posX + 3] < 255) { + weight = (weight * data[posX + 3]) / 250; + } + gxR += weight * data[posX]; + gxG += weight * data[posX + 1]; + gxB += weight * data[posX + 2]; + weights += weight; + } + } + + // + data2[x2] = gxR / weights; + data2[x2 + 1] = gxG / weights; + data2[x2 + 2] = gxB / weights; + data2[x2 + 3] = gxA / weightsAlpha; + } + } + + // + // clear and resize canvas + if (resizeCanvas) { + // + canvas.width = width; + canvas.height = height; + } else { + ctx.clearRect(0, 0, widthSource, heightSource); + } + + // + // draw + ctx.putImageData(img2, 0, 0); + } +} + +// +export function getExifRotation(imageBase64: string): number { + // + const view = new DataView(base64ToArrayBuffer(imageBase64)); + if (view.getUint16(0, false) !== 0xffd8) { + return -2; + } + + // + const length = view.byteLength; + let offset = 2; + while (offset < length) { + // + if (view.getUint16(offset + 2, false) <= 8) { + return -1; + } + + // + const marker = view.getUint16(offset, false); + offset += 2; + if (marker === 0xffe1) { + // + if (view.getUint32((offset += 2), false) !== 0x45786966) { + return -1; + } + + // + const little = view.getUint16((offset += 6), false) === 0x4949; + offset += view.getUint32(offset + 4, little); + const tags = view.getUint16(offset, little); + offset += 2; + + // + for (let i = 0; i < tags; i++) { + if (view.getUint16(offset + i * 12, little) === 0x0112) { + return view.getUint16(offset + i * 12 + 8, little); + } + } + // tslint:disable-next-line:no-bitwise + } else if ((marker & 0xff00) !== 0xff00) { + break; + } else { + offset += view.getUint16(offset, false); + } + } + return -1; +} + +export function base64ToArrayBuffer(imageBase64: string) { + // + imageBase64 = imageBase64.replace(/^data\:([^\;]+)\;base64,/gim, ''); + const binaryString = atob(imageBase64); + const len = binaryString.length; + const bytes = new Uint8Array(len); + + // + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + + // + return bytes.buffer; +} + +export function transformCanvas( + ctx: any, + orientation: number, + width: number, + height: number +) { + switch (orientation) { + case 2: + ctx.transform(-1, 0, 0, 1, width, 0); + break; + case 3: + ctx.transform(-1, 0, 0, -1, width, height); + break; + case 4: + ctx.transform(1, 0, 0, -1, 0, height); + break; + case 5: + ctx.transform(0, 1, 1, 0, 0, 0); + break; + case 6: + ctx.transform(0, 1, -1, 0, height, 0); + break; + case 7: + ctx.transform(0, -1, -1, 0, height, width); + break; + case 8: + ctx.transform(0, -1, 1, 0, 0, width); + break; + } +} + +export function arrayBufferToBase64( + arrBuffer: ArrayBuffer | SharedArrayBuffer +) { + // + try { + const base64 = btoa( + new Uint8Array(arrBuffer).reduce( + (data, byte) => data + String.fromCharCode(byte), + '' + ) + ); + + // + return base64; + } catch { + return null; + } +} + +export function arrayBufferToBase64Image( + sanitizer: DomSanitizer, + arrBuffer: ArrayBuffer | SharedArrayBuffer +) { + // + try { + // + const base64 = arrayBufferToBase64(arrBuffer); + + // + const imageData = 'data:image/png;base64,' + base64; + return sanitizer.bypassSecurityTrustUrl(imageData); + } catch { + return null; + } +} + +export function getBlobURL(fileName: string, fileBlob: Blob) { + const result = window.URL.createObjectURL(blobToFile(fileBlob, fileName)); + return result; +} + +export function toSafeUrl(sanitizer: DomSanitizer, url: any) { + return sanitizer.bypassSecurityTrustUrl(url); +} + +export function removeElement(element: any) { + // + if (element) { + element.remove(); + } + + // + element = null; +} + +export async function createCanvas(width: number, height: number) { + // + const result = document.createElement('canvas'); + + // + result.width = width; + result.height = height; + + // + return Promise.resolve(result); +} + +export async function clipImageToCanvas( + image: CanvasImageSource, + clipStartX: number, + clipStartY: number, + clipWidth: number, + clipHeight: number +) { + // + const canvas = await createCanvas(Math.abs(clipWidth), Math.abs(clipHeight)); + // + const context = canvas.getContext('2d'); + // + context.drawImage( + image, + clipStartX, + clipStartY, + clipWidth, + clipHeight, + 0, + 0, + canvas.width, + canvas.height + ); + // + removeElement(image); + return canvas; +} + +export async function dataUrlToImage( + dataUrl: string +): Promise { + // + return new Promise((resolve) => { + // + if (!dataUrl) { + resolve(null); + } + + // + const image = new Image(); + + // + image.onload = () => { + resolve(image); + }; + + // + image.onerror = () => { + resolve(null); + }; + + // + image.src = dataUrl; + }); +} + +export async function blobToImage(blob: Blob): Promise { + // + return new Promise((resolve) => { + // + if (!blob) { + resolve(null); + } + + // + const url = URL.createObjectURL(blob); + const image = new Image(); + + // + image.onload = () => { + // + URL.revokeObjectURL(url); + + // + resolve(image); + }; + + // + image.onerror = () => { + resolve(null); + }; + + // + image.src = url; + }); +} + +export async function canvasToDataUrl( + canvas: HTMLCanvasElement, + mimeType?: XMimeTypeIdentifier +) { + // + if (!canvas) { + return null; + } + + // + if (!mimeType) { + mimeType = XMimeType.ImagePNG; + } + + // + const result = canvas.toDataURL(mimeType); + + // + return Promise.resolve(result); +} + +export async function canvasToBlob( + canvas: HTMLCanvasElement, + mimeType?: XMimeTypeIdentifier, + quality?: number +): Promise { + // + return new Promise((resolve) => { + // + if (!canvas) { + resolve(null); + } + + // + if (!mimeType) { + mimeType = XMimeType.ImagePNG; + } + + // + if (!quality || quality > 1 || quality < 0) { + quality = 1; + } + + // + canvas.toBlob( + (blob) => { + resolve(blob); + }, + mimeType, + quality + ); + }); +} + +export async function downloadCanvas( + canvas: HTMLCanvasElement, + mimeType?: XMimeTypeIdentifier, + fileName?: string +) { + // + if (!canvas) { + return; + } + + // + if (!fileName) { + fileName = `${generateUuid()}.png`; + } + + // + const dataUrl = await canvasToDataUrl(canvas, mimeType); + if (!dataUrl) { + return; + } + + // + downloadFileByUrl(dataUrl, fileName); +} +//#endregion + +// +//#region Download File Actions ... +export function downloadArrayBufferedFile(fileName: string, fileBlob: Blob) { + // + try { + // + const url = getBlobURL(fileName, fileBlob); + + // + const downloadAnchor = document.createElement('a'); + downloadAnchor.href = url; + downloadAnchor.download = fileName; + + // + downloadAnchor.click(); + } catch {} +} + +export function downloadFileByUrl(url: string, fileName: string) { + // + const downloadAnchor = document.createElement('a'); + + // + downloadAnchor.href = url; + downloadAnchor.target = XRouterTarget.BLANK; + downloadAnchor.download = fileName; + + // + downloadAnchor.click(); + downloadAnchor.remove(); +} + +/** + * Method is use to download file. + * + * @param data - Array Buffer data + * @param type - type of the document. + * @param fileName - name of file. + * @param target - link target. + */ +export function downloadFile( + data: any, + mimeType?: string, + fileName?: string, + target: XRouterTargetIdentifier = XRouterTarget.BLANK +): void { + // + if (isNullOrUndefined(data)) { + return; + } + + // + // Wrap by try/catch ... + try { + // + // Prepare Blob based on mimeType ... + let blob: Blob = null; + if (isNullOrEmptyString(mimeType)) { + blob = new Blob([data]); + } else { + blob = new Blob([data], { type: mimeType }); + } + + // + // Prepare Url ... + const url = window.URL.createObjectURL(blob); + + // + // Create Anchor ... + const downloadAnchor = document.createElement('a'); + downloadAnchor.href = url; + + // + // Prepare Target ... + if (!isNullOrEmptyString(target)) { + downloadAnchor.target = target; + } + + // + // Attach FileName ... + if (!isNullOrEmptyString(fileName)) { + downloadAnchor.download = fileName; + } + + // + // Click to Download ... + downloadAnchor.click(); + } catch {} +} +//#endregion +//#endregion diff --git a/projects/x-framework-core/src/lib/tools/x-formatter.tools.ts b/projects/x-framework-core/src/lib/tools/x-formatter.tools.ts new file mode 100644 index 0000000..94dade9 --- /dev/null +++ b/projects/x-framework-core/src/lib/tools/x-formatter.tools.ts @@ -0,0 +1,287 @@ +import { hasChild } from './x-array.tools'; +import { removeAll } from './x-string.tools'; +import { throwException } from './x-exception.tools'; +import { isNullOrEmptyString } from './x-type-detect.tools'; +import { applyLocale } from '../typings/x-translation.typings'; +import { XExceptionIDs } from '../constants/x-exception.resources'; +import { XFormatterOption, XFormatterType } from '../typings/x.typings'; + +/** + * apply a format to a string ... + * + * @param options format options ... + * @param value a value which required to format ... + */ +export function applyFormat(options: XFormatterOption, value: string) { + // + value = value ? value.toString() : ''; + options = { + ...(!options ? { type: XFormatterType.ApplyLocale } : options), + }; + + // + const type = options.type; + + // + // Validate Options Based on Type ... + switch (type) { + // + case XFormatterType.ApplyLocale: + validateLocale(options); + break; + + // + case XFormatterType.FormatCurrencyAndApplyLocale: + validateLocale(options); + validateCurrency(options); + validateFormatter(options); + break; + + // + case XFormatterType.StringFormatterAndApplyLocale: + validateLocale(options); + validatePattern(options); + validateFormatter(options); + break; + + // + case XFormatterType.FormatCurrency: + validateCurrency(options); + validateFormatter(options); + break; + + // + case XFormatterType.StringFormatter: + validatePattern(options); + validateFormatter(options); + break; + } + + // + // Prepared Formatted Value ... + let result = isCurrencyType(options) + ? getCurrencyFormattedValue(options, value) + : isStringFormatterType(options) + ? getStringFormattedValue(options, value) + : value || ''; + + // + // Apply Locale if Localeable Input Type ... + if (isLocaleType(options)) { + result = getLocaleFormattedValue(options, result); + } + + // + return result; +} + +/** + * clear applied format ... + * + * @param options format options ... + * @param clearLocale clear applies locale ... + * @param value a value wich using to clear formatt ... + */ +export function clearFormat( + options: XFormatterOption, + clearLocale: boolean, + value: string +) { + // + let result = value.toString() || ''; + + // + const seperator = isNullOrEmptyString(options.seperator) + ? '' + : options.seperator; + + // + // Remove All occurence of seperator ... + result = removeAll(value, seperator, true); + + // + // Currency Lable ... + if (isCurrencyType(options) && !isNullOrEmptyString(options.currencyLable)) { + result = result.replace(options.currencyLable, '').trim(); + } + + // + // Change Locale to Default ... + if (isLocaleType(options)) { + result = !isNullOrEmptyString(result) + ? applyLocale( + result, + clearLocale ? 'en-US' : options.locale ? options.locale : 'en-US' + ) + : ''; + } + + // + return result; +} + +// +//#region applyFormat Function Helpers ... +// +function validateLocale(options: XFormatterOption) { + // + // || isNullOrEmptyString(options.locale) + if (!options) { + throwInvalidArgException(); + } +} + +// +function validateCurrency(options: XFormatterOption) { + // + // || isNullOrEmptyString(options.currencyLable) + if (!options) { + throwInvalidArgException(); + } +} + +// +function validateFormatter(options: XFormatterOption) { + // + // || isNullOrEmptyString(options.seperator) + if (!options) { + throwInvalidArgException(); + } +} + +// +function validatePattern(options: XFormatterOption) { + if (!options || !options.pattern) { + throwInvalidArgException(); + } +} + +// +function isCurrencyType(options: XFormatterOption) { + // + if (!options || !options.type) { + return false; + } + + // + switch (options.type) { + // + case XFormatterType.FormatCurrency: + case XFormatterType.FormatCurrencyAndApplyLocale: + return true; + + // + default: + return false; + } +} + +// +function isStringFormatterType(options: XFormatterOption) { + // + if (!options || !options.type) { + return false; + } + + // + switch (options.type) { + // + case XFormatterType.StringFormatter: + case XFormatterType.StringFormatterAndApplyLocale: + return true; + + // + default: + return false; + } +} + +// +function isLocaleType(options: XFormatterOption) { + // + if (!options || !options.type) { + return false; + } + + // + switch (options.type) { + // + case XFormatterType.ApplyLocale: + case XFormatterType.FormatCurrencyAndApplyLocale: + case XFormatterType.StringFormatterAndApplyLocale: + return true; + + // + default: + return false; + } +} + +// +function throwInvalidArgException() { + throwException(XExceptionIDs.InvalidArgs); +} + +// +function getLocaleFormattedValue(options: XFormatterOption, value: any) { + // + const mValue = value ? (value.toString() as string) : ''; + return applyLocale(mValue, options.locale); +} + +// +function getStringFormattedValue(options: XFormatterOption, value: any) { + // + const mValue = value ? (value.toString() as string) : ''; + + // + if (!hasChild(options.pattern)) { + return mValue; + } + + // + let mSource = mValue; + let mResultValue = ''; + options.pattern.forEach((l, i) => { + if (mSource.length >= l) { + // + const selected = mSource.substring(0, l); + + // + mResultValue += + i === options.pattern.length + ? selected + : `${selected}${options.seperator}`; + + // + mSource = mSource.substr(l, mValue.length); + } + }); + + // + if (mSource.length > 0) { + mResultValue += `${mSource}`; + } + + // + return mResultValue; +} + +// +function getCurrencyFormattedValue(options: XFormatterOption, value: any) { + // + const mValue = value ? ((+value).toString() as string) : ''; + const formattedValue = mValue.replace( + /\B(?=(\d{3})+(?!\d))/g, + options.seperator + ); + + // + const result = !isNullOrEmptyString(options.currencyLable) + ? `${formattedValue} ${options.currencyLable}` + : formattedValue; + + // + return result; +} +//#endregion diff --git a/projects/x-framework-core/src/lib/tools/x-number.tools.ts b/projects/x-framework-core/src/lib/tools/x-number.tools.ts new file mode 100644 index 0000000..740762e --- /dev/null +++ b/projects/x-framework-core/src/lib/tools/x-number.tools.ts @@ -0,0 +1,24 @@ +// +//#region Number Tools ... +/** + * generate a random number in specific range ... + */ +export function random(min?: number, max?: number) { + // + if (!min) { + min = 0; + } + + // + if (!max) { + max = 1000; + } + + // + min = Math.ceil(min); + max = Math.floor(max); + + // + return Math.floor(Math.random() * (max - min + 1)) + min; +} +//#endregion diff --git a/projects/x-framework-core/src/lib/tools/x-object.tools.ts b/projects/x-framework-core/src/lib/tools/x-object.tools.ts new file mode 100644 index 0000000..ef4cbab --- /dev/null +++ b/projects/x-framework-core/src/lib/tools/x-object.tools.ts @@ -0,0 +1,1125 @@ +import { + isDate, + isArray, + getType, + isObject, + DataTypes, + isNullOrUndefined, + isNullOrEmptyString, +} from './x-type-detect.tools'; +import { + XDeep, + XDeepPartial, + XBaseKeyValue, + XModelFieldTypeIndex, +} from '../typings/x.typings'; +import { Type } from '@angular/core'; +import { isSameDate } from './x-date-time.tools'; +import { countChild, hasChild, lastItem, onlyUniques } from './x-array.tools'; + +// +//#region Object Tools ... +/** + * this is an extension on objects which returns a not refrence copy of an object ... + */ +export function clone(obj: any) { + return Object.assign({}, obj); +} + +/** + * clone an object using JSON ... + * + * @param obj source object + */ +export function cloneJson(obj: T) { + return JSON.parse(JSON.stringify(obj)) as T; +} + +/** + * an extension on objects which convert an object to string ... + * + * @param obj source object + */ +export function toJsonString(obj: any) { + return JSON.stringify(obj); +} + +/** + * parse a json string to specific type ... + * + * @param jsonString a json string to parse ... + */ +export function parseJson(jsonString: string) { + return JSON.parse(jsonString) as T; +} + +/** + * convert object to json string and parse from it ... + * + * @param val any object ... + */ +export function fromJson(val: any): T { + return JSON.parse(JSON.stringify(val)) as T; +} + +/** + * assign a Value to an Object ... + * + * @param source source object which attached values to it ... + * @param keyValues key/value pairs to attach to source ... + */ +export function assign( + source: object, + ...keyValues: { key: string; value: any }[] +) { + // + keyValues.forEach((kv) => { + source[kv.key] = kv.value; + }); + + // + return source; +} + +/** + * an extension on objects which returns its keys array ... + * + * @param obj source object + */ +export function keys(obj: any) { + // + if (isNullOrUndefined(obj)) { + return []; + } + + // + return Object.keys(obj); +} + +/** + * return entries of objects ... + * + * @param obj object + */ +export function entries(obj: any) { + return Object.entries(obj); +} + +/** + * an extension on objects which returns its values array ... + * + * @param obj source object + */ +export function values(obj: any) { + return Object.values(obj); +} + +/** + * iterate over object keys ... + * + * @param obj source object + */ +export function iterate(obj: any) { + // + if (isObject(obj)) { + // + for (const property in obj) { + // + if (obj.hasOwnProperty(property)) { + // + if (isObject(obj[property])) { + iterate(obj[property]); + } + } + } + } +} + +/** + * retrieve prototype of an object ... + * + * @param object source object ... + */ +export function getPrototypeOf(object: any) { + if (Object.getPrototypeOf) { + return Object.getPrototypeOf(object); + } else { + return object.constructor.prototype; + } +} + +/** + * retrieve Object Key name ... + * + * @param propName Object Prop Name + */ +export function nameof(propName: keyof T) { + return propName; +} + +/** + * retrieve Object Key name ... + * + * @param propName Object Prop Name + */ +export function nameofType(type: Type, propName: keyof T) { + return propName.toString(); +} + +/** + * retrieve Object Key name ... + * + * @param propName Object Prop Name + */ +export function nameofInstance(obj: T, propName: keyof T) { + return propName.toString(); +} + +/** + * Retrieve Property Type ... + * + * @param obj + * @param key + * @returns + */ +export function getPropertyType(obj: T, key: K): T[K] { + return obj[key]; +} + +/** + * Merge two Object ... + * + * @param argA Object A + * @param argB Object B + */ +export function merge(argA: Partial, argB: Partial): Partial | any { + // + const result = {} as Partial; + + // + // Handle ARG A ... + if (argA) { + // + const aKeys = keys(argA); + if (hasChild(aKeys)) { + // + aKeys.forEach((k) => { + // + const aValue = argA[k]; + if (aValue) { + result[k] = aValue; + } + }); + } + } + + // + // Handle ARG B ... + if (argB) { + // + const bKeys = keys(argB); + if (hasChild(bKeys)) { + // + bKeys.forEach((k) => { + // + const bValue = argB[k]; + if (bValue) { + result[k] = bValue; + } + }); + } + } + + // + return result; +} + +export function mergeAll( + source: Partial, + ...args: Partial[] +): Partial | any { + // + let result = { + ...source, + } as Partial; + + // + args.forEach((arg) => { + result = merge(result, arg); + }); + + // + return result; +} + +/** + * extract partial content of an object ... + * + * @param source source object ... + * @param modelKeys the object keys for extracting ... + */ +export function toPartialObject( + source: T, + modelKeys: Array> +) { + // + if (!source || !hasChild(modelKeys)) { + return null; + } + + // + const result: Partial = {}; + modelKeys.forEach((k) => { + if (source[k]) { + assign(result, { key: k.toString(), value: source[k] }); + } + }); + + // + return result; +} + +/** + * convert a literal object to map ... + * + * @param obj an object to convert ... + */ +export function toMap(obj: any): Map { + return new Map(Object.entries(obj)); +} + +/** + * retrieve property value ... + * + * @param object the context object ... + * @param prop the property name ... + */ +export function getValue(object: T, prop: keyof T) { + // + if (object && object[prop]) { + return object[prop]; + } else { + return ''; + } +} + +/** + * retrieve and return an object Filed Names ... + * + * @param object the input object of a type ... + */ +export function getFieldNames(object: T): XModelFieldTypeIndex { + return mergeAll>( + {}, + ...(Object.keys(object).map((k) => { + return assign({}, { key: k, value: k }); + }) as XModelFieldTypeIndex[]) + ); +} + +/** + * retrieve a property value from an object by providing path + * + * @param object the model which going to parse + * @param propertyPath the . seperated property path + * @returns the object value for specified property + */ +export function getPropertyValue(object: T, propertyPath?: string) { + // + if (!object || isNullOrEmptyString(propertyPath)) { + return object; + } + + // + var prop, + props = propertyPath.split('.'); + + // + for (var i = 0, iLen = props.length - 1; i < iLen; i++) { + // + prop = props[i]; + + // + var candidate = object[prop]; + + // + if (candidate !== undefined) { + object = candidate; + } else { + break; + } + } + + // + return object[props[i]]; +} + +/** + * set value to specified nested object model ... + * + * @param object the model which going to parse + * @param propertyPath the . seperated property path + * @param value the value which needs to assign + * @param forceCreate if object is undefined create it + */ +export function setPropertyValue( + object: T, + propertyPath: string, + value?: any, + forceCreate?: boolean +) { + // + if (!object || isNullOrEmptyString(propertyPath)) { + return; + } + + // + var prop, + props = propertyPath.split('.'); + + // + for (var i = 0, iLen = props.length - 1; i < iLen; i++) { + // + prop = props[i]; + + // + var candidate = object[prop]; + + // + if (candidate !== undefined) { + object = candidate; + } else { + break; + } + } + + // + object[props[i]] = value; +} + +/** + * retrieve all available property paths of an object + * + * @param object the model which going to parse + * @param prefix the prefix path if reeds to assign + * @param extractSchema if true extracted arrays instead of objects + * @param containsChildsObjectHeaders if true the result will contains nested objects headers + * @returns an array of string which contains all property paths which mach the specified options + */ +export function getPropertyPaths( + object: T, + prefix?: string, + extractSchema: boolean = false, + containsChildsObjectHeaders: boolean = false +) { + // + // Create empty result Array ... + const result: string[] = []; + + // + // Check object exists ... + if (!object) { + return result; + } + + // + // get Object keys and check object contains key ... + const objectKeys = keys(object); + if (!hasChild(objectKeys)) { + return result; + } + + // + // loop through keys ... + for (let i = 0; i < objectKeys.length; i++) { + // + //#region Extarct required info ... + // + // Current key for process ... + const key = objectKeys[i]; + + // + // the master object value based on current key ... + const value = object[key]; + + // + // prepare prefix for current key ... + const keyPath = isNullOrEmptyString(prefix) ? key : `${prefix}.${key}`; + //#endregion + + // + // determine the value related to current key is an iterable object + // and required to handle it as nested object or not ... + const canResurse = isObject(value) || isArray(value); + if (canResurse) { + // + // if value must recursivly checked (Nested Objects) ... + + // + // if containsChildsObjectHeaders set to true add keyPath to result ... + if (!!containsChildsObjectHeaders) { + result.push(keyPath); + } + + // + // define current value empty paths array ... + let partPaths: string[] = []; + if (!!extractSchema) { + // + // if extractSchema set to true ... + + // + // For array nested values add .0 path az first argumentt ... + if (isArray(value)) { + result.push(`${keyPath}.0`); + } + + // + // get value path parts recursively based on type of value ... + partPaths = isArray(value) + ? // + // for Array Values retrieve only first childs type paths ... + getPropertyPaths( + (value as any[])[0], + `${keyPath}.0`, + extractSchema, + containsChildsObjectHeaders + ) + : // + // for Object Values retrieve all including keys ... + getPropertyPaths( + value, + keyPath, + extractSchema, + containsChildsObjectHeaders + ); + } else { + // + // if extractSchema set to false ... + // retrieve all available paths recursivly as an Object ... + partPaths = getPropertyPaths( + value, + keyPath, + extractSchema, + containsChildsObjectHeaders + ); + } + + // + // Check part Paths hasChild or not and attach proper values to result ... + if (hasChild(partPaths)) { + result.push(...partPaths); + } else { + result.push(keyPath); + } + } else { + // + // if value isprimitive type and no Nested Object ... + result.push(keyPath); + } + } + + // + // return result ... + return result; +} + +/** + * retrieve an specified object schema property paths ... + * + * @param object the model which going to parse + * @returns an array of string which contains all object's schema property paths + */ +export function getSchemaPaths(object: T) { + return getPropertyPaths(object, undefined, true, true); +} + +/** + * generate an object based on given property paths ... + * + * @param paths an string array which represent all property paths of an Object ... + * @param defaultValue determines default value which set to generated object's primitive properties ... + * @returns an specified type object + */ +export function generateObjectFromPaths( + paths: string[], + assginPropertyName: boolean = false, + defaultValue: any = null +): XDeep { + // + // Check paths contains childs ... + if (!hasChild(paths)) { + return null; + } + + // + // create empty result ... + let result = {} as XDeep; + + // + // loop through all paths ... + for (let i = 0; i < paths.length; i++) { + // + // current property path ... + const path = paths[i]; + + // + // filter all paths and exclude current path from them ... + const filteredPaths = paths.filter((p) => p.includes(path) && p !== path); + + // + // if filtered paths contains any childs this means current property is a nested object or array ... + const isNode = countChild(filteredPaths) > 0; + + // + // if filtered paths contains any childs including current path +'.0' this means current property is a nested array ... + const isArray = isNode && filteredPaths.includes(`${path}.0`); + + // + if (!!assginPropertyName) { + // + const pathParts = path.includes('.') ? path.split('.') : []; + defaultValue = hasChild(pathParts) ? lastItem(pathParts) : path; + } + + // + // set proper value to result object's current property as following: + // - if is a node set {}; + // - if is an array set []; + // - if primitive or else set defaultValue; + setPropertyValue(result, path, isNode ? (isArray ? [] : {}) : defaultValue); + } + + // + // return result object ... + return result; +} + +/** + * generate an object based on given property paths ... + * + * @param paths an string array which represent all property paths of an Object ... + * @param defaultValue determines default value which set to generated object's primitive properties ... + * @returns an specified type object + */ +export function generatePartialObjectFromPaths( + paths: string[], + assginPropertyName: boolean = false, + defaultValue: any = null +): XDeepPartial { + // + // Check paths contains childs ... + if (!hasChild(paths)) { + return null; + } + + // + // create empty result ... + let result = {} as XDeepPartial; + + // + // loop through all paths ... + for (let i = 0; i < paths.length; i++) { + // + // current property path ... + const path = paths[i]; + + // + // filter all paths and exclude current path from them ... + const filteredPaths = paths.filter((p) => p.includes(path) && p !== path); + + // + // if filtered paths contains any childs this means current property is a nested object or array ... + const isNode = countChild(filteredPaths) > 0; + + // + // if filtered paths contains any childs including current path +'.0' this means current property is a nested array ... + const isArray = isNode && filteredPaths.includes(`${path}.0`); + + // + if (!!assginPropertyName) { + // + const pathParts = path.includes('.') ? path.split('.') : []; + defaultValue = hasChild(pathParts) ? lastItem(pathParts) : path; + } + + // + // set proper value to result object's current property as following: + // - if is a node set {}; + // - if is an array set []; + // - if primitive or else set defaultValue; + setPropertyValue(result, path, isNode ? (isArray ? [] : {}) : defaultValue); + } + + // + // return result object ... + return result; +} + +/** + * generate an Schema object which represent given object ... + * + * @param object the model which going to parse + * @param defaultValue determines default value which set to Schema object's primitive properties ... + * @returns an Schema object + */ +export function getSchema( + object: T, + defaultValue: any = null +): XDeep { + // + const paths = getSchemaPaths(object); + const result = generateObjectFromPaths(paths, true, defaultValue); + + // + return result; +} + +/** + * generate an Schema object which represent given object ... + * + * @param object the model which going to parse + * @param defaultValue determines default value which set to Schema object's primitive properties ... + * @returns an Schema object + */ +export function getPartialSchema( + object: T, + defaultValue: any = null +): XDeepPartial { + // + const paths = getSchemaPaths(object); + const result = generatePartialObjectFromPaths(paths, true, defaultValue); + + // + return result; +} + +/** + * retrieve all exists path required to model actions ... + * + * @param object input object + * @returns an array of required paths for model actions + */ +export function extractModelInfoPaths(object: T) { + // + const schemaPaths = getSchemaPaths(object); + const result = onlyUniques(schemaPaths.map((part) => part.replace('.0', ''))); + + // + return result; +} + +/** + * retrieve an object of all available paths to model actions ... + * + * @param object input object + * @returns an object which filled by required paths + */ +export function extractModelInfoObject(object: T) { + // + const modelInfoPaths = extractModelInfoPaths(object); + const modelInfoKeyValuePairs = modelInfoPaths.map((path) => { + // + const result: XBaseKeyValue = { + key: path, + value: path, + }; + + // + return assign({}, result); + }); + const result = mergeAll({}, ...modelInfoKeyValuePairs) as any; + + // + return result; +} +//#endregion + +// +//#region Same Checkers ... +/** + * Check two object of Type is Same or not ... + * + * @param source the source object which required to type check + * @param dest the dest object which checked type with source + * @param forceMemberTypes determines all property types in each object must be same as type in other one. default is false + */ +export function isSameType(source: any, dest: any, forceMemberTypes = false) { + // + if (!source || !dest) { + return !source && !dest; + } + + // + const sType = getType(source); + const dType = getType(dest); + + // + if (sType !== dType) { + return false; + } + + // + if (sType === DataTypes.Object) { + // + // get Keys ... + const sourceKeys = keys(source); + const destKeys = keys(dest); + + // + if (countChild(sourceKeys) !== countChild(destKeys)) { + return false; + } + + // + const isAllSourceKeyExistsInDestKeys = sourceKeys.every((k) => + destKeys.includes(k) + ); + const isAllDestKeyExistsInSourceKeys = destKeys.every((k) => + sourceKeys.includes(k) + ); + + // + if (!isAllSourceKeyExistsInDestKeys || !isAllDestKeyExistsInSourceKeys) { + return false; + } + + // + if (forceMemberTypes) { + // + let memberTypesResult = true; + sourceKeys.forEach((k) => { + // + const kInSourceType = getType(source[k]); + const kInDestType = getType(dest[k]); + + // + if (kInDestType !== kInSourceType) { + memberTypesResult = false; + } + }); + + // + if (!memberTypesResult) { + return false; + } + } + } + + // + return true; +} + +/** + * Check Source and Destination array is Same ... + * + * @param source source array + * @param dest destination array + */ +export function isSameArray( + source: any[], + dest: any[], + forceCheckType?: boolean +): boolean { + // + if (!source || !dest) { + return (!source && !dest) as boolean; + } + + // + if (source.length !== dest.length) { + return false; + } + + // + if (!isSameType(source, dest)) { + return false; + } + + // + const result = source.sort().every((v, i) => { + // + const s = v; + const d = dest.sort()[i]; + + // + if (!!forceCheckType) { + if (!isSameType(s, d)) { + return false; + } + } + + // + if (isArray(s)) { + return isSameArray(s, d, forceCheckType); + } + + // + if (isObject(s)) { + return isSameObject(s, d, forceCheckType); + } + + // + return v === dest.sort()[i]; + }); + + // + return result; +} + +/** + * Check and Compare source and dest objects ... + * + * @param source source object + * @param dest destination object + * @param forceSameType check type of each member + */ +export function isSameObject( + source: object, + dest: object, + forceCheckType?: boolean +): boolean { + // + if (!source || !dest) { + return (!source && !dest) as boolean; + } + + // + const sKeysArr = Object.keys(source); + const dKeysArr = Object.keys(dest); + + // + const isSameKeys = isSameArray(sKeysArr, dKeysArr, forceCheckType); + if (!isSameKeys) { + return false; + } + + // + const result = sKeysArr.every((v, i) => { + // + const s = source[v]; + const d = dest[v]; + + // + if (!!forceCheckType) { + if (!isSameType(s, d)) { + return false; + } + } + + // + if (isArray(s)) { + return isSameArray(s, d, forceCheckType); + } + + // + if (isDate(s)) { + return isSameDate(s, d); + } + + // + if (isObject(s)) { + return isSameObject(s, d, forceCheckType); + } + + // + return s === d; + }); + + // + return result; +} + +/** + * Check Interface Types ... + */ +export function isOfType(value: any, keys: (keyof T)[]): value is T { + return ( + typeof value === "object" && + value !== null && + keys.every(k => k in value) + ); +} + +/** + * Create a Type Guard Function ... + * + * @param keys + * @returns + */ +export function typeGuard(keys: (keyof T)[]) { + return (value: any): value is T => + typeof value === "object" && + value !== null && + keys.every(k => k in value); +} +//#endregion + +// +//#region Enum Utils ... +/** + * return Keys of Enum ... + */ +// tslint:disable-next-line: ban-types +export function getKeys(T: Object) { + // + const result: string[] = []; + Object.keys(T).forEach((key) => { + // + const valProperty = parseInt(key, 10); + if (!isNaN(valProperty) && valProperty >= 0) { + result.push(T[valProperty]); + } + }); + + // + return result; +} + +/** + * return a cross value of key ... + */ +// tslint:disable-next-line:ban-types +export function getVal(T: Object, value: number | string) { + // + if (!T || isNullOrUndefined(value)) { + return null; + } + + // + if (!isNaN(parseInt(value.toString(), 10))) { + return T[value]; + } + + // + return null; +} + +/** + * convert an enumerable object to object .. + * + * @param T an enum object ... + */ +// tslint:disable-next-line:ban-types +export function toObject(T: Object) { + return Object.assign({}, T); +} +//#endregion + +// +//#region Component Tools ... +/** + * Access Specified Component Selector ... + * + * @param component + * @returns + */ +export function getComponentSelector(component: Type): string | null { + return (component as any).ɵcmp?.selectors?.[0]?.[0] ?? null; +} + +/** + * Access Specified Component's Name ... + * + * @param component + * @returns + */ +export function getComponentName(component: Type): string | null { + return component.toString(); +} + +/** + * Check Specified Value is inside Keys of Specified Type or not ... + * + * @param obj + * @param key + * @returns + */ +export function isXKeyOf( + obj: T, + key: PropertyKey +): key is keyof T { + return key in obj; +} +//#endregion + +// +//#region Wait For Element ... +/** + * Wait for Element Exists ... + * + * @param selector + * @returns + */ +export function waitForXElement(selector: string): Promise { + // + // Validate Selector ... + if (isNullOrEmptyString(selector)) { + return Promise.resolve(undefined); + } + + // + return new Promise((resolve) => { + // + // Try immediately + const el = document.querySelector(selector); + if (el) { + resolve(el); + return; + } + + // Otherwise, observe DOM changes + const observer = new MutationObserver(() => { + const el = document.querySelector(selector); + if (el) { + resolve(el); + observer.disconnect(); + } + }); + + observer.observe(document.body, { + childList: true, + subtree: true, + }); + }); +} + +/** + * Wait for Elements Exists ... + * + * @param selector + * @returns + */ +export function waitForXElements(selector: string): Promise { + // + // Validate Selector ... + if (isNullOrEmptyString(selector)) { + return Promise.resolve([]); + } + + // + return new Promise((resolve) => { + // + // Try immediately + const els = document.querySelectorAll(selector); + if (els) { + resolve(Array.from(els)); + return; + } + + // Otherwise, observe DOM changes + const observer = new MutationObserver(() => { + const els = document.querySelectorAll(selector); + if (els) { + resolve(Array.from(els)); + observer.disconnect(); + } + }); + + observer.observe(document.body, { + childList: true, + subtree: true, + }); + }); +} +//#endregion diff --git a/projects/x-framework-core/src/lib/tools/x-observable.tools.ts b/projects/x-framework-core/src/lib/tools/x-observable.tools.ts new file mode 100644 index 0000000..910facb --- /dev/null +++ b/projects/x-framework-core/src/lib/tools/x-observable.tools.ts @@ -0,0 +1,186 @@ +import { + of, + from, + Subject, + forkJoin, + Observable, + isObservable, + combineLatest, +} from 'rxjs'; +import { + map, + first, + finalize, + switchMap, + distinctUntilChanged, +} from 'rxjs/operators'; +import { + isObject, + isPromise, + isNullOrUndefined, + isNullOrEmptyString, +} from './x-type-detect.tools'; +import { keys } from './x-object.tools'; +import { EventEmitter, Type } from '@angular/core'; +import { hasChild, toArray } from './x-array.tools'; +import { + XHasAction, + XOneOrManyType, + XPropertyType, + XStandardType, +} from '../typings/x.typings'; + +// +//#region Handlers ... +export function applyFinally(obs: Observable, action: () => void) { + return obs.pipe(finalize(action)); +} + +export function hasValue(obs: Observable) { + return obs.pipe(map((res) => !isNullOrUndefined(res))); +} + +export function hasStringValue(obs: Observable) { + return obs.pipe(map((res) => !isNullOrEmptyString(res))); +} + +export function notValue(obs: Observable) { + return obs.pipe(map((res) => (isNullOrUndefined(res) ? true : !res))); +} + +export function toPromise(value: XStandardType) { + return toObservable(value).pipe(first()).toPromise(); +} + +export function toObservable(value: XStandardType) { + // + let result: Observable; + + // + if (isObservable(value)) { + result = value as Observable; + } else if (isPromise(value)) { + result = from(value as Promise); + } else { + result = of(value as T); + } + + // + return result.pipe(first()); +} + +/** + * filter an array by providing an observable condition ... + * @param list an instance of XStandardType> + * @param condition a condition + * @returns array result ... + */ +export function filterList( + list: XStandardType>, + condition: (item: T) => XStandardType +) { + return toObservable(list) + .pipe(map((valueType) => toArray(valueType))) + .pipe( + switchMap((array) => + combineLatest( + array.map((item) => + toObservable(condition(item)).pipe( + distinctUntilChanged(), + map((result) => (result ? item : null)) + ) + ) + ) + ), + map((resultArray) => resultArray.filter((action) => action)) + ); +} + +export function flatObject(input: T): Observable { + // + let result = {}; + let afterJoin = {}; + + // + const inputKeys = keys(input); + if (!hasChild(inputKeys)) { + return of(result); + } + + // + for (const key of inputKeys) { + // + const item = input[key]; + + // + const isItemObject = isObject(item); + if (isItemObject) { + // + if (item instanceof EventEmitter || item instanceof Subject) { + afterJoin[key] = item; + } else { + result[key] = flatObject(item); + } + } else { + result[key] = toObservable(item); + } + } + + // + return forkJoin(result).pipe( + map((res) => { + return { + // + ...(res as any), + + // + ...afterJoin, + }; + }) + ); +} + +export function nextSubjectWhenElementExists( + action: XPropertyType, 'action'>, + className: string, + actionProvider: Subject>, + delay: number = 200, + payload?: any +) { + // + // Validate ... + if ( + isNullOrUndefined(action) || + isNullOrEmptyString(className) || + isNullOrUndefined(actionProvider) + ) { + return; + } + + // + // Query to Select Target Elements ... + const elements: NodeListOf = document.querySelectorAll( + `.${className}` + ); + + // + if (isNullOrUndefined(elements) || !hasChild(elements)) { + // + setTimeout( + () => + nextSubjectWhenElementExists( + action, + className, + actionProvider, + delay, + payload + ), + delay + ); + return; + } + + // + actionProvider.next({ action, payload }); +} +//#endregion diff --git a/projects/x-framework-core/src/lib/tools/x-page.tools.ts b/projects/x-framework-core/src/lib/tools/x-page.tools.ts new file mode 100644 index 0000000..9e5f3f9 --- /dev/null +++ b/projects/x-framework-core/src/lib/tools/x-page.tools.ts @@ -0,0 +1,146 @@ +import { toPromise } from './x-observable.tools'; +import { XPage } from '../typings/x-page.typings'; +import { hasChild, toArray } from './x-array.tools'; +import { XOneOrManyType } from '../typings/x.typings'; +import { mergeRoutes, toNormalString } from './x-string.tools'; +import { isNullOrEmptyString, isNullOrUndefined } from './x-type-detect.tools'; + +export async function toFlatPages(pages: XOneOrManyType) { + // + const result: XPage[] = []; + + // + if (!pages || !hasChild(pages)) { + return result; + } + + // + const pagesArray = toArray(pages); + for (const page of pagesArray) { + // + const childsArray = await toPromise(page.childs); + const isParent = hasChild(childsArray); + + // + result.push(page); + + // + if (isParent) { + // + const childsRes = await toFlatPages(page.childs); + result.push(...childsRes); + } + } + + // + return result; +} + +export async function findPageByRoute( + pages: XOneOrManyType, + route: XOneOrManyType +) { + // + let result: XPage = null; + + // + if (!hasChild(pages) || !hasChild(route)) { + return result; + } + + // + const pRoute = mergeRoutes(route); + const pPages = await toFlatPages(pages); + if (!hasChild(pPages)) { + return result; + } + + // + for (const page of pPages) { + // + if (hasChild(page.route)) { + // + const pageRoute = mergeRoutes(page.route); + if ( + !isNullOrEmptyString(pageRoute) && + toNormalString(pageRoute) === toNormalString(pRoute) + ) { + result = page; + } + } + } + + // + return result; +} + +export async function containsPagesByRoute( + pages: XOneOrManyType, + route: XOneOrManyType +) { + // + const result: XPage[] = []; + + // + if (!hasChild(pages) || !hasChild(route)) { + return result; + } + + // + const pRoute = mergeRoutes(route); + if (isNullOrEmptyString(pRoute)) { + return result; + } + + // + const pPages = await toFlatPages(pages); + if (!hasChild(pPages)) { + return result; + } + + // + for (const page of pPages) { + // + if (hasChild(page.route)) { + // + const pageRoute = mergeRoutes(page.route); + if ( + !isNullOrEmptyString(pageRoute) && + toNormalString(pRoute).includes(toNormalString(pageRoute)) + ) { + result.push(page); + } + } + } + + // + return result; +} + +export async function parentPagesByRoute( + pages: XOneOrManyType, + route: XOneOrManyType +) { + // + let result: XPage[] = []; + + // + if (!hasChild(pages) || !hasChild(route)) { + return result; + } + + // + const parentPages = await containsPagesByRoute(pages, route); + const foundedPage = await findPageByRoute(pages, route); + if (!hasChild(parentPages) || isNullOrUndefined(foundedPage)) { + return result; + } + + // + result = parentPages.filter( + (i) => toNormalString(i.id) !== toNormalString(foundedPage.id) + ); + + // + return result; +} diff --git a/projects/x-framework-core/src/lib/tools/x-string.tools.ts b/projects/x-framework-core/src/lib/tools/x-string.tools.ts new file mode 100644 index 0000000..572781b --- /dev/null +++ b/projects/x-framework-core/src/lib/tools/x-string.tools.ts @@ -0,0 +1,516 @@ +import { Md5 } from 'md5-typescript'; +import { hasChild, toArray } from './x-array.tools'; +import { XOneOrManyType } from '../typings/x.typings'; +import { isNullOrEmptyString, isString } from './x-type-detect.tools'; + +// +export const XStringPlaceHolder = '(%$s)'; + +export const XEnDigits: string[] = [ + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', +]; +export const XFaDigits: string[] = [ + '۰', + '۱', + '۲', + '۳', + '۴', + '۵', + '۶', + '۷', + '۸', + '۹', +]; + +// +//#region String Utils ... +/** + * determines given string is empty or not ... + */ +export function isEmpty(value: string) { + // + if (isString(value) && value.length === 0) { + return true; + } + + // + return false; +} + +/** + * return digits of given string ... + */ +export function getDigits(value: string) { + // + if (!isString(value)) { + throw new Error('Invalid Parameter ...'); + } + + // + let result = ''; + for (let i = 0; i < value.length; i++) { + // + const chr = value.substr(i, 1); + if (chr in XEnDigits) { + // + result += chr; + } + } + + // + return result; +} + +/** + * return non digits of given string ... + */ +export function getNonDigits(value: string) { + // + if (!isString(value)) { + throw new Error('Invalid Parameter ...'); + } + + // + value = toEnNumbers(value); + + // + let result = ''; + for (let i = 0; i < value.length; i++) { + // + const chr = value.substr(i, 1); + if (!(chr in XEnDigits)) { + // + result += chr; + } + } + + // + return result; +} + +/** + * determine an expression contains only digits + */ +export function isOnlyDigits(value: string) { + // + if (isEmpty(value)) { + return false; + } + + // + const pattern = /^\d+$/; + return pattern.test(value); +} + +/** + * merge several parts of a url ... + * + * @param route an instance of url tree array ... + */ +export function mergeRoutes(route: XOneOrManyType) { + // + if (!hasChild(route)) { + return '/'; + } + + // + try { + return toArray(route).join('/').replace('//', '/'); + } catch {} + + // + return '/'; +} + +/** + * format string formatter ... + * + * @param value an string to format ... + * @param args the args for string formatter ... + */ +export function stringFormatter(value: string, ...args: any) { + // + if (!isString(value)) { + throw new Error('first arg must be an string ...'); + } + + // + if (args.length < 1) { + throw new Error('there mest be values ...'); + } + + // + const holder = XStringPlaceHolder; + let numberOfHolders = 0; + + // + if (value.length < holder.length) { + throw new Error('the sting doesnt contain any holder ...'); + } + + // + // tslint:disable-next-line:no-shadowed-variable + for (let i = 0; i < value.length; i++) { + // + const indexStr = value.substr(i, holder.length); + + // + if (indexStr === holder) { + numberOfHolders++; + } + } + + // + if (numberOfHolders !== args.length) { + throw new Error('theholders and parameters counts must be same ...'); + } + + // + // tslint:disable-next-line:prefer-for-of + for (let i = 0; i < args.length; i++) { + // + const a = value.indexOf(holder); + + // + value = + value.substr(0, a) + + args[i].toString() + + value.substr(a + holder.length, value.length - (a + holder.length)); + } + + // + return value; +} + +/** + * change all persian and arabic numers to english digits + */ +export function toEnNumbers(value: string): string { + // + let result = value ? value.toString() : ''; + + // + const persianNumbers = [ + /۰/g, + /۱/g, + /۲/g, + /۳/g, + /۴/g, + /۵/g, + /۶/g, + /۷/g, + /۸/g, + /۹/g, + ]; + const arabicNumbers = [ + /٠/g, + /١/g, + /٢/g, + /٣/g, + /٤/g, + /٥/g, + /٦/g, + /٧/g, + /٨/g, + /٩/g, + ]; + + // + for (let i = 0; i < 10; i++) { + if (!isNullOrEmptyString(result)) { + result = result + .replace(persianNumbers[i], i.toString()) + .replace(arabicNumbers[i], i.toString()); + } + } + + // + return result; +} + +/** + * change all english digits to persian digits + */ +export function toPersianNumbers(value: string): string { + // + let result = value; + + // + for (let i = 0; i < 10; i++) { + result = result.replace(new RegExp(i.toString(), 'g'), XFaDigits[i]); + } + + // + return result; +} + +/** + * remove all spaces on string value ... + * + * @param value an string value ... + */ +export function removeSpaces(value: string): string { + return value.replace(/\s/g, ''); +} + +/** + * remove margin spaces and make text lowercase + */ +export function toNormalString(value: string): string { + return isNullOrEmptyString(value) ? '' : value.trim().toLowerCase(); +} + +/** + * Capitalize an string ... + * + * @param value input string .. + * @returns string + */ +export function capital(value: string): string { + // + if (isNullOrEmptyString(value) || isNullOrEmptyString(value.trim())) { + return ''; + } + + // + value = value.trim(); + const firstChar = value.charAt(0).toLocaleUpperCase(); + const otherChars = value.slice(1); + + // + const result = `${firstChar}${otherChars}`; + return result; +} + +/** + * convert an string to Md5 ... + * + * @param value an string which going to convert + */ +export function toMd5(value: string) { + return isNullOrEmptyString(value) ? '' : Md5.init(value); +} + +/** + * convert given string to array buffer ... + * + * @param value the string which need to convert + */ +export function toArrayBuffer(value: string) { + // + if (!value || value.length === 0) { + return null; + } + + // + const stringLength = value.length; + const buffer = new ArrayBuffer(stringLength * 2); + const bufferView = new Uint16Array(buffer); + + // + for (let i = 0; i < stringLength; i++) { + bufferView[i] = value.charCodeAt(i); + } + + // + return buffer; +} + +/** + * convert byte array to string ... + * + * @param buffer the bytes array of an string + */ +export function fromArryBuffer(buffer: ArrayBuffer) { + return String.fromCharCode.apply(null, Array.from(new Uint16Array(buffer))); +} + +/** + * escape reg exp chars from an string ... + * + * @param source given string value to escape + * @returns escaped source value + */ +export function escapeRegExp(source: string) { + return source.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +} + +/** + * find all indexes of an string inside another and return them as an array ... + */ +export function findAllIndex(source: string, query: string) { + // + const result: number[] = []; + + // + const regExp = new RegExp(escapeRegExp(query), 'gi'); + let match: any; + // tslint:disable-next-line:no-conditional-assignment + while ((match = regExp.exec(source)) !== null) { + result.push(match.index); + } + + // + return result; +} + +/** + * insert a text to specific position on another text ... + */ +export function insertText(source: string, insertedText: string, pos: number) { + // + if (!source && !insertText) { + return ''; + } + + // + if (!source && insertText) { + return insertedText; + } + + // + if (pos > source.length) { + return source + insertedText; + } + + // + return [source.slice(0, pos), insertedText, source.slice(pos)].join(''); +} + +/** + * remove all occurence of an string inside of another string + */ +export function removeAll( + source: string, + removedString: string, + caseSensitive = false +) { + // + if (isNullOrEmptyString(removedString)) { + removedString = ''; + } + + // + const regExp = new RegExp( + removedString.toString(), + caseSensitive ? 'gi' : 'g' + ); + + // + if (isNullOrEmptyString(source)) { + source = ''; + } + + // + const result = source.toString().replace(regExp, ''); + + // + return result; +} + +/** + * extract all unique characters of an string ... + * + * @param value source string + */ +export function extractUniqueChars(value: string) { + // + if (isNullOrEmptyString(value)) { + return ''; + } + + // + let result = ''; + + // + // tslint:disable-next-line:prefer-for-of + for (let i = 0; i < value.length; i++) { + if (result.indexOf(value.charAt(i)) === -1) { + result += value[i]; + } + } + + // + return result; +} + +/** + * check an string contains unique chars or not ... + * + * @param value source string + */ +export function hasUniqueChar(value: string) { + // + if (isNullOrEmptyString(value)) { + return false; + } + + // + const uniqueChars = extractUniqueChars(value); + if (isNullOrEmptyString(uniqueChars)) { + return false; + } + + // + // const valueCharsArray = Array.from(value); + const uniqueCharsArray = Array.from(uniqueChars); + const findUniquArray = uniqueCharsArray.map((v) => { + // + const resultRes = findAllIndex(value, v); + + // + return resultRes && resultRes.length === 1; + }); + + // + const result = findUniquArray.includes(true); + + // + return result; +} + +/** + * convert an array object to list string ... + * + * @param values an array object to convert it to list string + * @param seperator list seperator ... + */ +export function toListString( + values: XOneOrManyType, + seperator: string = ',' +) { + // + const valueArr = toArray(values); + if (!hasChild(valueArr)) { + return ''; + } + + // + return valueArr.join(seperator); +} + +/** + * convert a list string to string array .. + * + * @param listString a list string + * @param seperator list seperator ... + */ +export function parseListString(listString: string, seperator: string = ',') { + // + if (!isNullOrEmptyString(listString)) { + return []; + } + + // + return listString.split(seperator); +} +//#endregion diff --git a/projects/x-framework-core/src/lib/tools/x-task.tools.ts b/projects/x-framework-core/src/lib/tools/x-task.tools.ts new file mode 100644 index 0000000..5f0650e --- /dev/null +++ b/projects/x-framework-core/src/lib/tools/x-task.tools.ts @@ -0,0 +1,89 @@ +export const taskIdStartKey = '_['; +export const taskIdEndKey = ']'; + +/** + * check if task name contains id ... + * + * @param key the task name ... + */ +export function hasTaskId(key: string | any) { + // + if (!key) { + return false; + } + + // + const mKey = key.toString() as string; + return mKey.includes(taskIdStartKey) && mKey.includes(taskIdEndKey); +} + +/** + * retrieve and extract the raw task name from a task name ... + * + * @param key the task name ... + */ +export function getTaskName(key: string | any) { + // + if (!key) { + return ''; + } + + // + const mKey = key.toString() as string; + + // + if (!hasTaskId(mKey)) { + return mKey; + } + + // + const taskId = getTaskId(mKey); + const taskIdString = `${taskIdStartKey}${taskId}${taskIdEndKey}`; + const result = mKey.replace(taskIdString, ''); + + // + return result; +} + +/** + * retrieve and extract the id inside a task name ... + * + * @param key the task name ... + */ +export function getTaskId(key: string | any) { + // + // Validate Args ... + if (!key || !hasTaskId(key)) { + return ''; + } + + // + // Task Id ... + let result = key.toString() as string; + + // + // Validating Info ... + let index = result.indexOf(taskIdStartKey); + + // + result = result.substr(index, result.length); + result = result.replace(taskIdStartKey, ''); + + // + index = result.indexOf(taskIdEndKey); + result = result.substring(0, index); + + // + // Return Result ... + return result; +} + +/** + * prepare a Task With ID and return ... + * + * @param key the Task Name ... + * @param id th ID ... + */ +export function setTaskId(key: string | any, id: string | any) { + return `${key.toString()}${taskIdStartKey}${id.toString()}${taskIdEndKey}`; +} diff --git a/projects/x-framework-core/src/lib/tools/x-to-form-data.ts b/projects/x-framework-core/src/lib/tools/x-to-form-data.ts new file mode 100644 index 0000000..2f6e0fa --- /dev/null +++ b/projects/x-framework-core/src/lib/tools/x-to-form-data.ts @@ -0,0 +1,148 @@ +import { + isDate, + isNull, + isFile, + isBlob, + isArray, + isObject, + isBoolean, + isUndefined, + isNullOrUndefined, + isNullOrEmptyString, +} from './x-type-detect.tools'; +import { blobToFile, generateFileName } from './x-file.tools'; + +export interface XToFormDataOptions { + /** + * include array indices in FormData keys + * defaults to false + */ + indices?: boolean; + + /** + * treat null values like undefined values and ignore them + * defaults to false + */ + nullsAsUndefineds?: boolean; + + /** + * convert true or false to 1 or 0 respectively + * defaults to false + */ + booleansAsIntegers?: boolean; +} + +/** + * Convert an Object to FormData ... + * + * @param source the object which going to convert to FormData ... + * @param options Converter Options ... + * @param formData an exists FormData instance which apply object to it ... + * @param keysPrefix key prefix for FormData Values ... + */ +export function toFormData( + source: any, + options?: XToFormDataOptions, + formData?: FormData, + keysPrefix?: string +) { + // + //#region Prepare Options ... + options = options || {}; + + options.indices = isNullOrUndefined(options.indices) ? true : options.indices; + + options.nullsAsUndefineds = isNullOrUndefined(options.nullsAsUndefineds) + ? false + : options.nullsAsUndefineds; + + options.booleansAsIntegers = isNullOrUndefined(options.booleansAsIntegers) + ? false + : options.booleansAsIntegers; + //#endregion + + // + formData = formData || new FormData(); + + // + //#region Do Convertion ... + if (isUndefined(source)) { + return formData; + } else if (isNull(source)) { + // + if (!options.nullsAsUndefineds) { + formData.append(keysPrefix, ''); + } + } else if (isBoolean(source)) { + // + if (options.booleansAsIntegers) { + formData.append(keysPrefix, source ? '1' : '0'); + } else { + formData.append(keysPrefix, source); + } + } else if (isArray(source)) { + // + if (source.length) { + source.forEach((value: any, index: number) => { + const key = keysPrefix + '[' + (options.indices ? index : '') + ']'; + formData = toFormData(value, options, formData, key); + }); + } + } else if (isDate(source)) { + formData.append(keysPrefix, source.toISOString()); + } else if (isObject(source) && !isFile(source) && !isBlob(source)) { + // + Object.keys(source).forEach((prop) => { + // + const value = source[prop]; + + // + if (isArray(value)) { + while (prop.length > 2 && prop.lastIndexOf('[]') === prop.length - 2) { + prop = prop.substring(0, prop.length - 2); + } + } + + // + const key = keysPrefix ? keysPrefix + '[' + prop + ']' : prop; + + // + formData = toFormData(value, options, formData, key); + }); + } else if (isFile(source) || isBlob(source)) { + // + let file: File = null; + if (isFile(source)) { + file = source as File; + } else if (isBlob(source)) { + // + const blob = source as Blob; + const fileName = generateFileName(blob); + file = blobToFile(blob, fileName); + } + + // + formData.append(keysPrefix, file, file.name); + } else { + formData.append(keysPrefix, source.toString()); + } + //#endregion + + return formData; +} + +/** + * log an instance of FormData object's Content in Console ... + */ +export async function getFormDataContent(source: FormData) { + return await new Response(source).text(); +} + +/** + * log an instance of FormData object's Content in Console ... + */ +export async function logFormData(source: FormData) { + // + const result = await getFormDataContent(source); + console.log(result); +} diff --git a/projects/x-framework-core/src/lib/tools/x-type-convert.tools.ts b/projects/x-framework-core/src/lib/tools/x-type-convert.tools.ts new file mode 100644 index 0000000..dd6850f --- /dev/null +++ b/projects/x-framework-core/src/lib/tools/x-type-convert.tools.ts @@ -0,0 +1,59 @@ +// +//#region Type Converter ... +/** + * convert given object to string ... + */ +export function toXString(object: any) { + // + try { + // + return String(object); + } catch (error) { + // + throw error; + } +} + +/** + * convert given object to number ... + */ +export function toNumber(object: any) { + // + try { + // + return Number(object); + } catch (error) { + // + throw error; + } +} + +/** + * convert given object to date ... + */ +export function toDate(object: any) { + // + try { + // + return new Date(object); + } catch (error) { + // + throw error; + } +} + +/** + * convert given object to boolean ... + */ +export function toBoolean(object: any) { + // + try { + // + // tslint:disable-next-line:no-eval + return Boolean(eval(object)); + } catch (error) { + // + throw error; + } +} +//#endregion diff --git a/projects/x-framework-core/src/lib/tools/x-type-defaults.tools.ts b/projects/x-framework-core/src/lib/tools/x-type-defaults.tools.ts new file mode 100644 index 0000000..542f8e3 --- /dev/null +++ b/projects/x-framework-core/src/lib/tools/x-type-defaults.tools.ts @@ -0,0 +1,65 @@ +import { + isArray, + isObject, + isNumber, + isString, + isBoolean, +} from './x-type-detect.tools'; +import { XStandardType } from '../typings/x.typings'; +import { toObservable, toPromise } from './x-observable.tools'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +export async function getDefaultValueAsync(t: XStandardType): Promise { + // + let result: any; + + // + const tVal = await toPromise(toObservable(t)); + + // + if (isBoolean(tVal)) { + result = false; + } else if (isNumber(tVal)) { + result = 0; + } else if (isString(tVal)) { + result = ''; + } else if (isObject(tVal) && !isArray(tVal)) { + result = null; + } else if (isArray(tVal)) { + result = []; + } else { + result = undefined; + } + + // + return result as T; +} + +export function getDefaultValue(t: XStandardType): Observable { + // + return toObservable(t).pipe( + map((tVal) => { + // + let result: any; + + // + if (isBoolean(tVal)) { + result = false; + } else if (isNumber(tVal)) { + result = 0; + } else if (isString(tVal)) { + result = ''; + } else if (isObject(tVal) && !isArray(tVal)) { + result = null; + } else if (isArray(tVal)) { + result = []; + } else { + result = undefined; + } + + // + return result as T; + }) + ); +} diff --git a/projects/x-framework-core/src/lib/tools/x-type-detect.tools.ts b/projects/x-framework-core/src/lib/tools/x-type-detect.tools.ts new file mode 100644 index 0000000..bb134ed --- /dev/null +++ b/projects/x-framework-core/src/lib/tools/x-type-detect.tools.ts @@ -0,0 +1,215 @@ +/** + * Custom Data Types ... + */ +export const DataTypes = { + Null: 'null', + Undefined: 'undefined', + String: 'string', + Number: 'number', + Boolean: 'boolean', + Object: 'object', + Function: 'function', +}; + +/** + * determines type of an object + * + * @param variable specified object + */ +export function getType(variable: any) { + return typeof variable; +} + +// +//#region Type Checker ... +/** + * determine a var is null or not ... + */ +export function isNull(variable: any) { + // + if ( + !variable && + (variable === null || getType(variable) === DataTypes.Null) + ) { + return true; + } + + // + return false; +} + +/** + * determine a var is undefined ... + */ +export function isUndefined(variable: any) { + // + if (getType(variable) === DataTypes.Undefined) { + return true; + } + + // + return false; +} + +/** + * setermine a var is null or undefined ... + */ +export function isNullOrUndefined(variable: any) { + // + if (isNull(variable) || isUndefined(variable)) { + return true; + } + + // + return false; +} + +export function isNullOrEmptyString(variable: string) { + // + if (isNull(variable) || isUndefined(variable) || variable.length === 0) { + return true; + } + + // + return false; +} + +export function isNullOrZeroLength(variable: any[]) { + // + if (isNullOrUndefined(variable)) { + return true; + } + + // + return variable.length === 0; +} + +/** + * determine a var is string or not ... + */ +export function isString(variable: any) { + // + if (getType(variable) === DataTypes.String) { + return true; + } + + // + return false; +} + +/** + * determine a var is Number or not ... + */ +export function isNumber(variable: any) { + // + if (getType(variable) === DataTypes.Number) { + return true; + } + + // + var numValue = parseInt(variable, 10); + if (isNaN(numValue)) { + return false; + } + + // + return true; +} + +/** + * determine a var is boolean or not ... + */ +export function isBoolean(variable: any) { + // + if (getType(variable) === DataTypes.Boolean) { + return true; + } + + // + return false; +} + +/** + * determine a object is Array or not ... + * + * IMPORTANT: an array must be an object ... + */ +export function isArray(value: any) { + return value && Array.isArray(value); +} + +/** + * determine a var is object or not ... + */ +export function isObject(variable: any) { + // + return ( + variable && + getType(variable) === DataTypes.Object && + variable === Object(variable) + ); +} + +/** + * determine a var is function or not ... + */ +export function isFunction(variable: any) { + // + if (getType(variable) === DataTypes.Function) { + return true; + } + + // + return false; +} + +/** + * determine a var is Blob ... + */ +export function isBlob(value: any) { + return ( + value && + typeof value.size === 'number' && + typeof value.type === 'string' && + typeof value.slice === 'function' + ); +} + +/** + * check an input arg is a date object or not ... + * + * @param source any arg which required to check + */ +export function isDate(source: any) { + return ( + source && + isObject(source) && + (source instanceof Date || + isFunction(source.getMonth) || + Object.prototype.toString.call(source) === '[object Date]') + ); +} + +/** + * determine a var is File ... + */ +export function isFile(value: any) { + return ( + isBlob(value) && + typeof value.name === 'string' && + (typeof value.lastModifiedDate === 'object' || + typeof value.lastModified === 'number') + ); +} + +/** + * check wheather an object is promise or not ... + */ +export function isPromise(variable: any) { + if (!isNullOrUndefined(variable) && isFunction(variable.then)) { + return true; + } + + return false; +} +//#endregion diff --git a/projects/x-framework-core/src/lib/tools/x-uuid-tools.ts b/projects/x-framework-core/src/lib/tools/x-uuid-tools.ts new file mode 100644 index 0000000..0e94a3d --- /dev/null +++ b/projects/x-framework-core/src/lib/tools/x-uuid-tools.ts @@ -0,0 +1,16 @@ +import { v4 as uuidv4 } from 'uuid'; +import { v5 as uuidv5 } from 'uuid'; + +export const DEFAULT_NAMESPACE = 'SaherElm IT Center'; +export function generateUuid( + prefix?: string, + content?: string, + nameSpace: string = DEFAULT_NAMESPACE +) { + // + const uId = content ? uuidv5(content, nameSpace) : uuidv4(); + const result = prefix ? `${prefix}_${uId}` : uId; + + // + return result; +} diff --git a/projects/x-framework-core/src/lib/typings/x-base-component.typings.ts b/projects/x-framework-core/src/lib/typings/x-base-component.typings.ts new file mode 100644 index 0000000..9373788 --- /dev/null +++ b/projects/x-framework-core/src/lib/typings/x-base-component.typings.ts @@ -0,0 +1 @@ +export const DEFAULT_EXPORT_TYPING_BASE = 'default'; diff --git a/projects/x-framework-core/src/lib/typings/x-color.typings.ts b/projects/x-framework-core/src/lib/typings/x-color.typings.ts new file mode 100644 index 0000000..53dce3b --- /dev/null +++ b/projects/x-framework-core/src/lib/typings/x-color.typings.ts @@ -0,0 +1,105 @@ +// +import { XBaseKeyValue } from './x.typings'; + +//#region Color Tools ... +export interface XColorDescribtor { + name: XColor; + brightnessLevel: XColorBrightnessLevel; + isContrast?: boolean; +} + +export enum XColor { + Primary = 'primary', + Secondary = 'secondary', + Tertiary = 'tertiary', + Success = 'success', + Warning = 'warning', + Danger = 'danger', + Dark = 'dark', + Medium = 'medium', + Light = 'light', + // + Transparent = 'cb-inherit', +} + +export enum XColorWithBrightness { + // + PrimaryTint = 'primary-tint', + Primary = 'primary', + PrimaryShade = 'primary-shade', + // + SecondaryTint = 'secondary-tint', + Secondary = 'secondary', + SecondaryShade = 'secondary-shade', + // + TertiaryTint = 'tertiary-tint', + Tertiary = 'tertiary', + TertiaryShade = 'tertiary-shade', + // + SuccessTint = 'success-tint', + Success = 'success', + SuccessShade = 'success-shade', + // + WarningTint = 'warning-tint', + Warning = 'warning', + WarningShade = 'warning-shade', + // + DangerTint = 'danger-tint', + Danger = 'danger', + DangerShade = 'danger-shade', + // + DarkTint = 'dark-tint', + Dark = 'dark', + DarkShade = 'dark-shade', + // + MediumTint = 'medium-tint', + Medium = 'medium', + MediumShade = 'medium-shade', + // + LightTint = 'light-tint', + Light = 'light', + LightShade = 'light-shade', + // + Transparent = 'cb-inherit', +} + +export type XColorIdentifier = XColor | XColorWithBrightness | string; + +export enum XColorBrightnessLevel { + Tint = 'tint', + Regular = 'regular', + Shade = 'shade', +} + +export interface XRGBAColor { + r?: number; + g?: number; + b?: number; + a?: number; +} + +export interface XHSVAColor { + h?: number; + s?: number; + v?: number; + a?: number; +} + +export interface XHSLAColor { + h?: number; + s?: number; + l?: number; + a?: number; +} + +export enum XColorFormat { + XHEX = 'hex', + XRGBA = 'rgba', + XHSLA = 'hsla', + XHSVA = 'hsva', +} + +export type XColorFormatIdentifier = XColorFormat | string; + +export interface XColorKeyValue extends XBaseKeyValue {} +//#endregion diff --git a/projects/x-framework-core/src/lib/typings/x-date-time.typings.ts b/projects/x-framework-core/src/lib/typings/x-date-time.typings.ts new file mode 100644 index 0000000..6450926 --- /dev/null +++ b/projects/x-framework-core/src/lib/typings/x-date-time.typings.ts @@ -0,0 +1,5 @@ +export enum XUnitOfTime { + Year, + Month, + Day +} diff --git a/projects/x-framework-core/src/lib/typings/x-exception.typings.ts b/projects/x-framework-core/src/lib/typings/x-exception.typings.ts new file mode 100644 index 0000000..b872833 --- /dev/null +++ b/projects/x-framework-core/src/lib/typings/x-exception.typings.ts @@ -0,0 +1,13 @@ +import { XExceptionIDs, Exceptions } from '../constants/x-exception.resources'; + +export interface XException { + id: number; + message: string; + payload?: any[]; +} + +export type XExceptionIndex = { + [key in keyof typeof XExceptionIDs]: XException; +}; + +export type XExceptions = keyof typeof Exceptions; diff --git a/projects/x-framework-core/src/lib/typings/x-manager.typings.ts b/projects/x-framework-core/src/lib/typings/x-manager.typings.ts new file mode 100644 index 0000000..e02eb49 --- /dev/null +++ b/projects/x-framework-core/src/lib/typings/x-manager.typings.ts @@ -0,0 +1,20 @@ +export enum XUiEvent { + // + StartLoading, + StopLoading, + + // + Disabled, + Enabled, + + // + Locked, + UnLocked, + + // + BackPrevented, + BackReleased, + + // + Ready +} diff --git a/projects/x-framework-core/src/lib/typings/x-notification.typings.ts b/projects/x-framework-core/src/lib/typings/x-notification.typings.ts new file mode 100644 index 0000000..7902ead --- /dev/null +++ b/projects/x-framework-core/src/lib/typings/x-notification.typings.ts @@ -0,0 +1,262 @@ +import { + AlertInput, + ToastOptions, + ModalOptions, + AlertOptions, + ComponentRef, + PickerOptions, + PopoverOptions, + LoadingOptions, + ActionSheetOptions, +} from '@ionic/core'; +import { XResourceIdentifier } from './x-translation.typings'; +import { XExceptions, XException } from './x-exception.typings'; +import { XSpinnerIdentifier, XOneOrManyType } from './x.typings'; +import { XExceptionIDs } from '../constants/x-exception.resources'; +import { XResourceIDs } from '../constants/x-translation.resources'; + +// +//#region Typings for Dialogs and Notifications ... +/** + * Action Type for Notifications in Audio Service + */ +export enum XActionType { + User, + System, + Capture, +} + +/** + * represent a single notification sound for specific actions + */ +export interface XNotificationSound { + type: XActionType; + resourcePath: string; +} + +/** + * represent a group of notification sounds and coresponding actions + */ +export interface NotificationSoundIndex { + [name: string]: XNotificationSound; +} + +export interface XNotificationParams { + type?: XActionType; + opt?: ToastOptions; + playSound?: boolean; + dissmissable: boolean; + onDidDismiss?: () => void; + onWillDismiss?: () => void; +} + +export interface XTypedNotificationParams extends XNotificationParams { + message: string | XResourceIDs; +} + +export interface XErrorNotificationParams extends XNotificationParams { + exception: XExceptionIDs | XExceptions | XException | any; +} + +type XXActionModalOptions = Pick< + Partial, + | 'id' + | 'animated' + | 'backdropDismiss' + | 'cssClass' + | 'keyboardClose' + | 'translucent' +>; +export interface XActionModalOptions extends XXActionModalOptions { + // + type?: XActionType; + playSound?: boolean; + header?: XResourceIdentifier; + subHeader?: XResourceIdentifier; + buttons?: XModalButton[]; + + // + hasYes?: boolean; + yesRole?: XModalButtonRoleIdentifier; + yesHandler?: () => boolean | void | Promise; + + hasNo?: boolean; + noRole?: XModalButtonRoleIdentifier; + noHandler?: () => boolean | void | Promise; + + hasOk?: boolean; + okRole?: XModalButtonRoleIdentifier; + okHandler?: () => boolean | void | Promise; + + hasRetry?: boolean; + retryRole?: XModalButtonRoleIdentifier; + retryHandler?: () => boolean | void | Promise; + + hasAccept?: boolean; + acceptRole?: XModalButtonRoleIdentifier; + acceptHandler?: () => boolean | void | Promise; + + hasCancel?: boolean; + cancelRole?: XModalButtonRoleIdentifier; + cancelHandler?: () => boolean | void | Promise; + + hasDismiss?: boolean; + dismissRole?: XModalButtonRoleIdentifier; + dismissHandler?: () => boolean | void | Promise; +} + +type XXModalOptions = Pick< + Partial, + | 'componentProps' + | 'showBackdrop' + | 'backdropDismiss' + | 'cssClass' + | 'animated' + | 'component' + | 'keyboardClose' + | 'id' +>; +export interface XModalOptions extends XXModalOptions { + component: ComponentRef; + autoHeight?: boolean; + fullScreen?: boolean; +} + +type XXPopOverOptions = Pick< + Partial, + | 'animated' + | 'backdropDismiss' + | 'componentProps' + | 'cssClass' + | 'component' + | 'id' + | 'showBackdrop' + | 'translucent' + | 'keyboardClose' + | 'event' +>; +export interface XPopoverOptions extends XXPopOverOptions { + component: ComponentRef; +} + +type XXLoadingOptions = Pick< + Partial, + | 'animated' + | 'backdropDismiss' + | 'cssClass' + | 'duration' + | 'id' + | 'keyboardClose' + | 'showBackdrop' + | 'translucent' + | 'message' +>; + +// tslint:disable-next-line:no-empty-interface +export interface XLoadingOptions extends XXLoadingOptions { + spinner?: XSpinnerIdentifier; +} + +type XXAlertOptions = Pick< + Partial, + | 'animated' + | 'translucent' + | 'backdropDismiss' + | 'id' + | 'keyboardClose' + | 'cssClass' +>; + +export enum XModalButtonRole { + Cancel = 'cancel', + Selected = 'selected', + Destructive = 'destructive', +} + +export type XModalButtonRoleIdentifier = XModalButtonRole | string; + +export interface XModalButton { + text: XResourceIdentifier; + role?: XModalButtonRoleIdentifier; + cssClass?: XOneOrManyType; + handler?: ( + value?: any + ) => + | boolean + | void + | { + [key: string]: any; + } + | any; +} + +export type XAlertInputType = + | 'date' + | 'email' + | 'number' + | 'password' + | 'search' + | 'tel' + | 'text' + | 'url' + | 'time' + | 'checkbox' + | 'radio'; + +export interface XModalInput { + id?: string; + value?: any; + name?: string; + checked?: boolean; + disabled?: boolean; + min?: string | number; + max?: string | number; + type?: XAlertInputType; + label?: XResourceIdentifier; + placeholder?: XResourceIdentifier; + handler?: (input: AlertInput) => void; +} + +export interface XAlertOptions extends XXAlertOptions { + header?: XResourceIdentifier; + subHeader?: XResourceIdentifier; + message: XResourceIdentifier; + inputs?: XModalInput[]; + buttons?: XModalButton[]; +} + +type XXPickerOptions = Pick< + Partial, + 'animated' | 'backdropDismiss' | 'id' | 'cssClass' | 'keyboardClose' +>; + +export interface XPickerColumn { + name: string; + align?: string; + selectedIndex?: number; + prevSelected?: number; + prefix?: string; + suffix?: string; + options: XPickerColumnOption[]; + cssClass?: string | string[]; + columnWidth?: string; + prefixWidth?: string; + suffixWidth?: string; + optionsWidth?: string; + refresh?: () => void; +} + +export interface XPickerColumnOption { + text?: XResourceIdentifier; + value: any; + disabled?: boolean; + duration?: number; + transform?: string; + selected?: boolean; +} + +export interface XPickerOptions extends XXPickerOptions { + buttons?: XModalButton[]; + columns: XPickerColumn[]; +} +//#endregion diff --git a/projects/x-framework-core/src/lib/typings/x-page.typings.ts b/projects/x-framework-core/src/lib/typings/x-page.typings.ts new file mode 100644 index 0000000..fd2cca1 --- /dev/null +++ b/projects/x-framework-core/src/lib/typings/x-page.typings.ts @@ -0,0 +1,13 @@ +import { XOneOrManyType } from './x.typings'; +import { XResourceIdentifier } from './x-translation.typings'; + +export interface XPage { + id: string; + name: string; + icon?: string; + baseRoute: string; + title: XResourceIdentifier; + route: XOneOrManyType; + childs?: XOneOrManyType; + description?: XResourceIdentifier; +} diff --git a/projects/x-framework-core/src/lib/typings/x-query.typings.ts b/projects/x-framework-core/src/lib/typings/x-query.typings.ts new file mode 100644 index 0000000..d2cd6a4 --- /dev/null +++ b/projects/x-framework-core/src/lib/typings/x-query.typings.ts @@ -0,0 +1,10 @@ +export interface XBaseDataPagerQuery { + filter?: string; + pageNumber: number; + rowsInPage: number; +} + +export interface XBaseDataPagerQueryResult { + count: number; + items: T[]; +} diff --git a/projects/x-framework-core/src/lib/typings/x-setting.typings.ts b/projects/x-framework-core/src/lib/typings/x-setting.typings.ts new file mode 100644 index 0000000..5222ecd --- /dev/null +++ b/projects/x-framework-core/src/lib/typings/x-setting.typings.ts @@ -0,0 +1,26 @@ +export type XOrientation = 'landscape' | 'portrait'; + +export enum XDeviceTypes { + Unknown, + Mobile, + Tablet, + Desktop +} + +export interface XDeviceInfo { + os: string; + osVersion: string; + browser: string; + userAgent: string; + deviceType: XDeviceTypes; + userId?: number; + + identifier?: string; + token?: string; +} + +export type XCssProperties = + | 'x-app-bar-color' + | 'ui-direction' + | 'toolbar-height' + | string; diff --git a/projects/x-framework-core/src/lib/typings/x-storage.typings.ts b/projects/x-framework-core/src/lib/typings/x-storage.typings.ts new file mode 100644 index 0000000..bb5aa2e --- /dev/null +++ b/projects/x-framework-core/src/lib/typings/x-storage.typings.ts @@ -0,0 +1,7 @@ +/** + * represent a single item which can be stored ... + */ +export interface XStorageItem { + key: string; + value: string; +} diff --git a/projects/x-framework-core/src/lib/typings/x-task-manager.typings.ts b/projects/x-framework-core/src/lib/typings/x-task-manager.typings.ts new file mode 100644 index 0000000..2ebe71d --- /dev/null +++ b/projects/x-framework-core/src/lib/typings/x-task-manager.typings.ts @@ -0,0 +1,18 @@ +import { Observable } from 'rxjs'; +import { XException } from './x-exception.typings'; + +export enum XTaskPriority { + High, + Medium, + Regular, + Weak +} + +export interface XTask { + affectLoading?: boolean; + name: string; + priority: XTaskPriority; + task: Observable; + result?: T; + error?: XException; +} diff --git a/projects/x-framework-core/src/lib/typings/x-theme-manager.typings.ts b/projects/x-framework-core/src/lib/typings/x-theme-manager.typings.ts new file mode 100644 index 0000000..f4f8be7 --- /dev/null +++ b/projects/x-framework-core/src/lib/typings/x-theme-manager.typings.ts @@ -0,0 +1,135 @@ +export enum XThemeType { + Light = 'light', + Dark = 'dark', +} + +export type XThemeTypeIdentifier = XThemeType | string; + +export enum XThemeCssVars { + // + //#region Primary ... + XColorPrimary = 'x-color-primary', + XColorPrimaryContrast = 'x-color-primary-contrast', + + XColorPrimaryShade = 'x-color-primary-shade', + XColorPrimaryShadeContrast = 'x-color-primary-shade-contrast', + + XColorPrimaryTint = 'x-color-primary-tint', + XColorPrimaryTintContrast = 'x-color-primary-tint-contrast', + //#endregion + + // + //#region Secondary ... + XColorSecondary = 'x-color-secondary', + XColorSecondaryContrast = 'x-color-secondary-contrast', + + XColorSecondaryShade = 'x-color-secondary-shade', + XColorSecondaryShadeContrast = 'x-color-secondary-shade-contrast', + + XColorSecondaryTint = 'x-color-secondary-tint', + XColorSecondaryTintContrast = 'x-color-secondary-tint-contrast', + //#endregion + + // + //#region Tertiary ... + XColorTertiary = 'x-color-tertiary', + XColorTertiaryContrast = 'x-color-tertiary-contrast', + + XColorTertiaryShade = 'x-color-tertiary-shade', + XColorTertiaryShadeContrast = 'x-color-tertiary-shade-contrast', + + XColorTertiaryTint = 'x-color-tertiary-tint', + XColorTertiaryTintContrast = 'x-color-tertiary-tint-contrast', + //#endregion + + // + //#region Success ... + XColorSuccess = 'x-color-success', + XColorSuccessContrast = 'x-color-success-contrast', + + XColorSuccessShade = 'x-color-success-shade', + XColorSuccessShadeContrast = 'x-color-success-shade-contrast', + + XColorSuccessTint = 'x-color-success-tint', + XColorSuccessTintContrast = 'x-color-success-tint-contrast', + //#endregion + + // + //#region Warning ... + XColorWarning = 'x-color-warning', + XColorWarningContrast = 'x-color-warning-contrast', + + XColorWarningShade = 'x-color-warning-shade', + XColorWarningShadeContrast = 'x-color-warning-shade-contrast', + + XColorWarningTint = 'x-color-warning-tint', + XColorWarningTintContrast = 'x-color-warning-tint-contrast', + //#endregion + + // + //#region Danger ... + XColorDanger = 'x-color-danger', + XColorDangerContrast = 'x-color-danger-contrast', + + XColorDangerShade = 'x-color-danger-shade', + XColorDangerShadeContrast = 'x-color-danger-shade-contrast', + + XColorDangerTint = 'x-color-danger-tint', + XColorDangerTintContrast = 'x-color-danger-tint-contrast', + //#endregion + + // + //#region Dark ... + XColorDark = 'x-color-dark', + XColorDarkContrast = 'x-color-dark-contrast', + + XColorDarkShade = 'x-color-dark-shade', + XColorDarkShadeContrast = 'x-color-dark-shade-contrast', + + XColorDarkTint = 'x-color-dark-tint', + XColorDarkTintContrast = 'x-color-dark-tint-contrast', + //#endregion + + // + //#region Medium ... + XColorMedium = 'x-color-medium', + XColorMediumContrast = 'x-color-medium-contrast', + + XColorMediumShade = 'x-color-medium-shade', + XColorMediumShadeContrast = 'x-color-medium-shade-contrast', + + XColorMediumTint = 'x-color-medium-tint', + XColorMediumTintContrast = 'x-color-medium-tint-contrast', + //#endregion + + // + //#region Light ... + XColorLight = 'x-color-light', + XColorLightContrast = 'x-color-light-contrast', + + XColorLightShade = 'x-color-light-shade', + XColorLightShadeContrast = 'x-color-light-shade-contrast', + + XColorLightTint = 'x-color-light-tint', + XColorLightTintContrast = 'x-color-light-tint-contrast', + //#endregion +} + +export type XThemeCssVarsIdentifier = XThemeCssVars | string; + +export type XThemeDescriptorColors = keyof typeof XThemeCssVars; + +export type XThemeColors = { + [key in XThemeDescriptorColors]: string; +}; + +export interface XThemeDescriptor { + type: XThemeTypeIdentifier; + colors: XThemeColors; +} + +export interface XThemePack { + name: string; + light: XThemeDescriptor; + dark: XThemeDescriptor; +} diff --git a/projects/x-framework-core/src/lib/typings/x-translation.typings.ts b/projects/x-framework-core/src/lib/typings/x-translation.typings.ts new file mode 100644 index 0000000..8e43813 --- /dev/null +++ b/projects/x-framework-core/src/lib/typings/x-translation.typings.ts @@ -0,0 +1,51 @@ +import { XResourceIDs } from '../constants/x-translation.resources'; +import { toEnNumbers, toPersianNumbers } from '../tools/x-string.tools'; + +export declare type XLanguageDirection = 'ltr' | 'rtl'; +export declare type XLocale = 'en-US' | 'fa-IR'; + +export interface XLanguageDescriptor { + name: string; + locale: XLocale; + language: string; + direction: XLanguageDirection; + resources?: XTranslationItem[]; +} + +export interface XLanguageDescriptorIndex { + [name: string]: XLanguageDescriptor; +} + +export type XTranslationId = keyof typeof XResourceIDs; + +export interface XTranslationItem { + id: XTranslationId | string; + value: string; +} + +export interface XTranslationIndex { + [name: string]: XTranslationItem; +} + +export interface XLocalesIndex { + [key: string]: XTranslationIndex; +} + +export interface GetDateProps { + date?: Date; + locale?: XLocale; + dateFormat?: string; + toUtc?: boolean; +} + +export function applyLocale(content: string, locale: XLocale = 'en-US') { + switch (locale) { + case 'en-US': + return toEnNumbers(content); + + case 'fa-IR': + return toPersianNumbers(content); + } +} + +export type XResourceIdentifier = XResourceIDs | XTranslationId | string; diff --git a/projects/x-framework-core/src/lib/typings/x.typings.ts b/projects/x-framework-core/src/lib/typings/x.typings.ts new file mode 100644 index 0000000..6d1aa9d --- /dev/null +++ b/projects/x-framework-core/src/lib/typings/x.typings.ts @@ -0,0 +1,229 @@ +import { Observable } from 'rxjs'; +import { XFileBlob } from '../tools/x-file.tools'; +import { XLocale } from './x-translation.typings'; +import { CountryCode, PhoneNumber } from 'libphonenumber-js'; + +// +//#region General ... +export type XStandardType = Observable | Promise | T; + +export type XOneOrManyType = T | T[]; + +export type XFormControlState = 'VALID' | 'INVALID' | 'PENDING' | 'DISABLED'; + +export type XRouterDirection = 'root' | 'forward' | 'back'; + +export type XFileReaderInputType = File | Blob | XFileBlob | string; + +export enum XRouterTarget { + TOP = '_top', + SELF = '_self', + BLANK = '_blank', + PARENT = '_parent', +} + +export type XRouterTargetIdentifier = XRouterTarget | string; + +export enum XMimeType { + // + //#region Applications ... + ApplicationOctetStream = 'application/octet-stream', + ApplicationPDF = 'application/pdf', + ApplicationZIP = 'application/zip', + ApplicationAtomXML = 'application/atom+xml', + ApplicationGZIP = 'application/gzip', + ApplicationJSON = 'application/json', + ApplicationJWT = 'application/jwt', + ApplicationMP4 = 'application/mp4', + ApplicationRTF = 'application/rtf', + ApplicationSQL = 'application/sql', + ApplicationWSDLXML = 'application/wsdl+xml', + ApplicationXWWWFormURLEncoded = 'application/x-www-form-urlencoded', + ApplicationXX509CaCert = 'application/x-x509-ca-cert', + ApplicationXML = 'application/xml', + ApplicationJavascript = 'application/javascript', + ApplicationECMAScript = 'application/ecmascript', + //#endregion + + // + //#region Audios ... + Audio3GPP = 'audio/3gpp', + AudioAAC = 'audio/aac', + AudioAMR = 'audio/AMR', + AudioMPA = 'audio/MPA', + AudioMP4 = 'audio/mp4', + AudioMPEG = 'audio/mpeg', + AudioOGG = 'audio/ogg', + //#endregion + + // + //#region Fonts ... + FontCollection = 'font/collection', + FontOTF = 'font/otf', + FontSFNT = 'font/sfnt', + FontTTF = 'font/ttf', + FontWOFF = 'font/woff', + FontWOFF2 = 'font/woff2', + //#endregion + + // + //#region Image ... + ImageBMP = 'image/bmp', + ImageGIF = 'image/gif', + ImageJPEG = 'image/jpg', + ImagePNG = 'image/png', + ImageSVG = 'image/svg+xml', + ImageTIFF = 'image/tiff', + ImageWMF = 'image/wmf', + //#endregion + + // + //#region Messages ... + MessageSIP = 'message/sip', + MessageSIPFrag = 'message/sipfrag', + Message = 'message/', + //#endregion + + // + //#region Multipart ... + MultipartEncrypted = 'multipart/encrypted', + MultipartFormData = 'multipart/form-data', + MultipartHeaderSET = 'multipart/header-set', + MultipartRelated = 'multipart/related', + MultipartReport = 'multipart/report', + MultipartSigned = 'multipart/signed', + MultipartVoiceMessage = 'multipart/voice-message', + //#endregion + + // + //#region Text ... + TextCalendar = 'text/calendar', + TextCSS = 'text/css', + TextCSV = 'text/csv', + TextCSVSchema = 'text/csv-schema', + TextDirectory = 'text/directory', + TextDNS = 'text/dns', + TextHTML = 'text/html', + TextECMAScript = 'text/ecmascript', + TextJavascript = 'text/javascript', + TextMarkdown = 'text/markdown', + TextPlain = 'text/plain', + TextRTF = 'text/rtf', + TextStrings = 'text/strings', + TextVCard = 'text/vcard', + TextXML = 'text/xml', + //#endregion + + // + //#region Video ... + Video3GPP = 'video/3gpp', + Video3GPP2 = 'video/3gpp2', + VideoH265 = 'video/H265', + VideoJPEG = 'video/JPEG', + VideoMP4 = 'video/mp4', + VideoMPV = 'video/MPV', + VideoMPEG = 'video/mpeg', + VideoOGG = 'video/ogg', + VideoQuicktime = 'video/quicktime', + //#endregion +} + +export type XMimeTypeIdentifier = XMimeType | string; + +export enum XSpinner { + Lines = 'lines', + LinesSmall = 'lines-small', + Dots = 'dots', + Bubbles = 'bubbles', + Circles = 'circles', + Circular = 'circular', + Crescent = 'crescent', +} + +export type XSpinnerIdentifier = XSpinner | string | any; + +export type XCountryCode = CountryCode; + +export type XPhoneNumber = PhoneNumber; + +export enum XFormatterType { + ApplyLocale = 'apply_locale', + FormatCurrency = 'format_currency', + StringFormatter = 'string_formatter', + FormatCurrencyAndApplyLocale = 'format_currency_apply_locale', + StringFormatterAndApplyLocale = 'string_formatter_apply_locale', +} + +export interface XFormatterOption { + type: XFormatterType; + locale?: XLocale; + seperator?: string; + currencyLable?: string; + pattern?: number[]; +} + +export enum XDialogResult { + Cancel, + Ok, + Retry, + Opened, + Restart, +} + +export interface XBaseKeyValue { + key: string; + value: T; +} + +export interface XKeyValue { + key: TKey; + value: TValue; +} + +export interface XHasAction { + action: T; + payload?: any +} + +export type XKey = keyof T; +export type XKeysOfXType = { [K in keyof T]: T[K] extends U ? K : never }[keyof T]; +export type XStringKeys = Extract, string>; +export type XValueOf = T[XKey]; +export type XRef = { value: T }; +export type XPropertyType = T[K]; +export type XModelFieldTypeIndex = { + [key in XKey]: string; +}; +export type XPartialModelFieldNames = XOneOrManyType< + Partial>> +>; +export type XDeep = keyof T & T extends object + ? { [P in keyof T]: XDeep } + : T; +export type XDeepPartial = T extends Function + ? T + : T extends object + ? { [P in keyof T]?: XDeepPartial } + : T; + +export type XModelKeyType = keyof Partial | string; + +export type XModelFieldsType = XOneOrManyType>; + +export type XModelKeyProvider = ( + key: XModelKeyType +) => XStandardType; + +export type XModelValueProvider = ( + key: XModelKeyType, + value: any +) => XStandardType; + +export interface XModelProperty + extends XKeyValue, any> { + prefix?: string; +} + +export type XGConstructor = new (...args: any[]) => T; +export type XConstructor = new (...args: any[]) => {}; +//#endregion diff --git a/projects/x-framework-core/src/lib/validators/x-general-validators.ts b/projects/x-framework-core/src/lib/validators/x-general-validators.ts new file mode 100644 index 0000000..a816413 --- /dev/null +++ b/projects/x-framework-core/src/lib/validators/x-general-validators.ts @@ -0,0 +1,316 @@ +import { + XPhoneNumber, + XCountryCode, + XOneOrManyType +} from '../typings/x.typings'; +import { + isNullOrUndefined, + isNullOrEmptyString +} from '../tools/x-type-detect.tools'; +import { hasChild, toArray } from '../tools/x-array.tools'; +import { throwException } from '../tools/x-exception.tools'; +import { parsePhoneNumberFromString } from 'libphonenumber-js'; +import { XExceptionIDs } from '../constants/x-exception.resources'; + +// +//#region Custom Type Checker and Converters ... +/** + * check specified value is Valid URL Address or not ... + * + * @param value specified string which required to check + */ +export function isValidURL(value: string) { + // + const pattern = new RegExp( + '^(https?:\\/\\/)?' + // protocol + '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.?)+[a-z]{2,}|' + // domain name + '((\\d{1,3}\\.){3}\\d{1,3}))' + // ip (v4) address + '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port + '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string + '(\\#[-a-z\\d_]*)?$', + 'i' + ); + + // + return pattern.test(value); +} + +/** + * check specified value is Valid URL Address or not ... + * + * @param value specified string which required to check + */ +export function isValidEmail(value: string) { + // + const pattern = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}/gim; + + // + return pattern.test(value); +} + +/** + * check specified value is Valid Phone Number or not ... + * + * @param value specified string which required to check + * @param defaultCountryCode an optional value which used for country code if not exists in value + */ +export function isValidPhoneNumber( + value: string, + defaultCountryCode?: XCountryCode +) { + // + const mParsedPhoneNumber = parsePhoneNumber(value, defaultCountryCode); + if (!mParsedPhoneNumber) { + return false; + } + + // + return mParsedPhoneNumber.isValid(); +} + +/** + * parse an string as phone number and extract several information of it ... + * + * @param value specified string is represent phone number ... + * @param defaultCountryCode an optional value which used for country code if not exists in value + */ +export function parsePhoneNumber( + value: string, + defaultCountryCode?: XCountryCode +): XPhoneNumber { + // + try { + return parsePhoneNumberFromString(value, defaultCountryCode); + } catch { + return null; + } +} + +/** + * normalize specified phone number and format it ... + * + * @param value specified string which required to normalized ... + * @param defaultCountryCode an optional value which used for country code if not exists in value + */ +export function normalizePhoneNumber( + value: string, + defaultCountryCode?: XCountryCode +) { + // + try { + // + const parsedNumber = parsePhoneNumber(value, defaultCountryCode); + if (!parsedNumber) { + throw new Error('can not Parse Phone Number ...'); + } + + // + return parsedNumber.number.toString(); + } catch { + return null; + } +} +//#endregion + +// +//#region Data Validators from Several Types ... +export class XValidators { + // + private static throwInvalidArgsException() { + throwException(XExceptionIDs.InvalidArgs); + } + + /** + * check args not Null or Undefined ... + * + * @param args any args which required to check + */ + static validateNotNull(...args: any[]) { + // + const isValid = args.every(arg => !isNullOrUndefined(arg)); + if (!isValid) { + XValidators.throwInvalidArgsException(); + } + } + + /** + * check args not empty string or null or undefined ... + * + * @param args any args which required to check + */ + static validateNotEmpty(...args: string[]) { + // + const isValid = args.every(arg => !isNullOrEmptyString(arg)); + if (!isValid) { + XValidators.throwInvalidArgsException(); + } + } + + /** + * check args must have childs ... + * + * @param args any args which required to check + */ + static validateHasChilds(...args: any[][]) { + // + const isValid = args.every(arg => hasChild(arg)); + if (!isValid) { + XValidators.throwInvalidArgsException(); + } + } + + /** + * check all args not equal to specified values ... + * + * @param values a single or collection value which going to compare + * @param args any args which required to check + */ + static validateNotEquals(values: XOneOrManyType, ...args: T[]) { + // + const mValues = toArray(values); + + // + const isValid = args.every(arg => !mValues.includes(arg)); + if (!isValid) { + XValidators.throwInvalidArgsException(); + } + } + + /** + * check all args equal to specified values ... + * + * @param values a single or collection value which going to compare + * @param args any args which required to check + */ + static validateEquals(values: XOneOrManyType, ...args: T[]) { + // + const mValues = toArray(values); + + // + const isValid = args.every(arg => mValues.includes(arg)); + if (!isValid) { + XValidators.throwInvalidArgsException(); + } + } + + /** + * check all args bigger than specified values ... + * + * @param values a single or collection value which going to compare + * @param args any args which required to check + */ + static validateBiggerThan(values: number | number[], ...args: number[]) { + // + const mValues = toArray(values); + + // + const isValid = args.every(arg => mValues.every(n => arg > n)); + if (!isValid) { + XValidators.throwInvalidArgsException(); + } + } + + /** + * check all args less than specified values ... + * + * @param values a single or collection value which going to compare + * @param args any args which required to check + */ + static validateLessThan(values: number | number[], ...args: number[]) { + // + const mValues = toArray(values); + + // + const isValid = args.every(arg => mValues.every(n => arg < n)); + if (!isValid) { + XValidators.throwInvalidArgsException(); + } + } + + /** + * check all args bigger than or equals to specified values ... + * + * @param values a single or collection value which going to compare + * @param args any args which required to check + */ + static validateBiggerThanOrEquals( + values: number | number[], + ...args: number[] + ) { + // + const mValues = toArray(values); + + // + const isValid = args.every(arg => mValues.every(n => arg >= n)); + if (!isValid) { + XValidators.throwInvalidArgsException(); + } + } + + /** + * check all args less than or equals to specified values ... + * + * @param values a single or collection value which going to compare + * @param args any args which required to check + */ + static validateLessThanOrEquals( + values: number | number[], + ...args: number[] + ) { + // + const mValues = toArray(values); + + // + const isValid = args.every(arg => mValues.every(n => arg <= n)); + if (!isValid) { + XValidators.throwInvalidArgsException(); + } + } + + /** + * check specified value is Valid URL Address or not ... + * + * @param value specified string which required to check + */ + static validateURL(value: string) { + // + XValidators.validateNotEmpty(value); + + // + if (!isValidURL(value)) { + throwException(XExceptionIDs.InvalidEmailAddress); + } + } + + /** + * check specified value is Valid Email Address or not ... + * + * @param value specified string which required to check + */ + static validateEmailAddress(value: string) { + // + XValidators.validateNotEmpty(value); + + // + if (!isValidEmail(value)) { + throwException(XExceptionIDs.InvalidEmailAddress); + } + } + + /** + * check specified value is Valid Phone Number or not ... + * + * @param value specified string which required to check + * @param defaultCountryCode an optional value which used for country code if not exists in value + */ + static validatePhoneNumber(value: string, defaultCountryCode?: XCountryCode) { + // + XValidators.validateNotEmpty(value); + + // + if (!isValidPhoneNumber(value, defaultCountryCode)) { + throwException(XExceptionIDs.InvalidMobileNumber); + } + } +} +//#endregion diff --git a/projects/x-framework-core/src/lib/validators/x-valid-email.directive.ts b/projects/x-framework-core/src/lib/validators/x-valid-email.directive.ts new file mode 100644 index 0000000..cfd58f1 --- /dev/null +++ b/projects/x-framework-core/src/lib/validators/x-valid-email.directive.ts @@ -0,0 +1,29 @@ +import { Directive } from '@angular/core'; +import { isValidEmail } from './x-general-validators'; +import { Validator, AbstractControl, ValidationErrors } from '@angular/forms'; + +@Directive({ + // tslint:disable-next-line:directive-selector + selector: '[xValidEmail]' +}) +export class XValidEmailDirective implements Validator { + // + private errorObject = { + xValidEmail: true + }; + + validate(control: AbstractControl): ValidationErrors { + // + const sourceValue = + control && control.value ? (control.value as string) : null; + + // + let isValid = false; + try { + isValid = isValidEmail(sourceValue); + } catch {} + + // + return !isValid ? this.errorObject : null; + } +} diff --git a/projects/x-framework-core/src/lib/validators/x-valid-phone-number.directive.ts b/projects/x-framework-core/src/lib/validators/x-valid-phone-number.directive.ts new file mode 100644 index 0000000..6e01e4a --- /dev/null +++ b/projects/x-framework-core/src/lib/validators/x-valid-phone-number.directive.ts @@ -0,0 +1,36 @@ +import { XCountryCode } from '../typings/x.typings'; +import { Directive, Input, Inject } from '@angular/core'; +import { isValidPhoneNumber } from './x-general-validators'; +import { X_FRAMEWORK_CORE_CONFIG } from '../tokens/x-injectable-tokens'; +import { XFrameworkCoreConfig } from '../config/x-framework-core.config'; +import { Validator, AbstractControl, ValidationErrors } from '@angular/forms'; + +@Directive({ + // tslint:disable-next-line:directive-selector + selector: '[xValidPhoneNumber]' +}) +export class XValidPhoneNumberDirective implements Validator { + // + private errorObject = { + xValidMobile: true + }; + + // + @Input() + defaultCountryCode: XCountryCode = this.config.defaultCountryCode; + + constructor( + @Inject(X_FRAMEWORK_CORE_CONFIG) private config: XFrameworkCoreConfig + ) {} + + validate(control: AbstractControl): ValidationErrors { + // + const value = control && control.value ? (control.value as string) : null; + + // + const isValid = isValidPhoneNumber(value, this.defaultCountryCode); + + // + return !isValid ? this.errorObject : null; + } +} diff --git a/projects/x-framework-core/src/lib/validators/x-valid-url.directive.ts b/projects/x-framework-core/src/lib/validators/x-valid-url.directive.ts new file mode 100644 index 0000000..9c963a4 --- /dev/null +++ b/projects/x-framework-core/src/lib/validators/x-valid-url.directive.ts @@ -0,0 +1,36 @@ +import { Directive, Input } from '@angular/core'; +import { isValidURL } from './x-general-validators'; +import { Validator, AbstractControl, ValidationErrors } from '@angular/forms'; + +@Directive({ + // tslint:disable-next-line:directive-selector + selector: '[xValidUrl]' +}) +export class XValidUrlDirective implements Validator { + // + private errorObject = { + xValidUrl: true + }; + + @Input() + isOptional: boolean; + + constructor() {} + + validate(control: AbstractControl): ValidationErrors { + // + const sourceValue = + control && control.value ? (control.value as string) : null; + + // + let isValid = false; + if (!sourceValue && this.isOptional) { + isValid = true; + } else { + isValid = sourceValue && isValidURL(sourceValue); + } + + // + return !isValid ? this.errorObject : null; + } +} diff --git a/projects/x-framework-core/src/lib/validators/x-value-match.directive.ts b/projects/x-framework-core/src/lib/validators/x-value-match.directive.ts new file mode 100644 index 0000000..3267a08 --- /dev/null +++ b/projects/x-framework-core/src/lib/validators/x-value-match.directive.ts @@ -0,0 +1,57 @@ +import { + NgModel, + Validator, + AbstractControl, + ValidationErrors +} from '@angular/forms'; +import { Directive, Input } from '@angular/core'; +import { isNullOrEmptyString } from '../tools/x-type-detect.tools'; + +@Directive({ + // tslint:disable-next-line:directive-selector + selector: '[xValueMatch]' +}) +export class XValueMatchDirective implements Validator { + private errorObject = { + xValueMatch: true + }; + + // + @Input('xValueMatch') + matchTo: NgModel; + + // + validate(control: AbstractControl): ValidationErrors { + // + if (!control) { + return this.errorObject; + } + const value = control.value as string; + + // + if (!this.matchTo) { + return this.errorObject; + } + const destControl = this.matchTo.control; + if (!destControl) { + return this.errorObject; + } + + // + const destValue = destControl.value; + + // + const isValid = + !isNullOrEmptyString(destValue) && + !isNullOrEmptyString(value) && + value === destValue; + + // + if (isValid && destControl.invalid) { + destControl.updateValueAndValidity(); + } + + // + return !isValid ? this.errorObject : null; + } +} diff --git a/projects/x-framework-core/src/lib/x-framework-core.module.ts b/projects/x-framework-core/src/lib/x-framework-core.module.ts new file mode 100644 index 0000000..3f9b97b --- /dev/null +++ b/projects/x-framework-core/src/lib/x-framework-core.module.ts @@ -0,0 +1,21 @@ +import { NgModule } from '@angular/core'; +import { IonicModule } from '@ionic/angular' +import { CommonModule } from '@angular/common'; +import { XDirectivesModule } from './modules/x-directives.module'; +import { HAMMER_GESTURE_CONFIG } from '@angular/platform-browser'; +import { XHammerGestureConfig } from './classes/x-hammer-gesture-config'; + +@NgModule({ + declarations: [], + providers: [ + // + // Gesture Config ... + { + provide: HAMMER_GESTURE_CONFIG, + useClass: XHammerGestureConfig, + }, + ], + imports: [CommonModule, IonicModule, XDirectivesModule], + exports: [CommonModule, IonicModule, XDirectivesModule], +}) +export class XFrameworkCoreModule {} diff --git a/projects/x-framework-core/src/public-api.ts b/projects/x-framework-core/src/public-api.ts new file mode 100644 index 0000000..fca2687 --- /dev/null +++ b/projects/x-framework-core/src/public-api.ts @@ -0,0 +1,131 @@ +/* + * Public API Surface of x-framework-core + */ + +//#region Classes ... +export * from './lib/classes/x-loggable'; +export * from './lib/classes/x-base.service'; +export * from './lib/classes/x-renderer.class'; +export * from './lib/classes/x-persian-date-adapter'; +//#endregion + +// +//#region Constants ... +export * from './lib/constants/x-file.enum'; +export * from './lib/constants/x.constants'; +export * from './lib/constants/x-crud.enum'; +export * from './lib/constants/x-param.enum'; +export * from './lib/constants/x-header.enum'; +export * from './lib/constants/x-border.enum'; +export * from './lib/constants/x-setting.resources'; +export * from './lib/constants/x-exception.resources'; +export * from './lib/constants/x-translation.resources'; +//#endregion + +// +//#region Guards ... +export * from './lib/guards/x-can-deactivate.guard'; +//#endregion + +// +//#region Models ... +export * from './lib/models/x-base-dto'; +export * from './lib/models/x-grouped-data.dto'; +export * from './lib/models/x-base.indexed.dto'; +export { + XBehaviourEvent, + XBehaviourEventType, +} from './lib/directives/x-scroll-behavior.directive'; +//#endregion + +// +//#region Tools ... +export * from './lib/tools/x-task.tools'; +export * from './lib/tools/x-page.tools'; +export * from './lib/tools/x-uuid-tools'; +export * from './lib/tools/x-file.tools'; +export * from './lib/tools/x-array.tools'; +export * from './lib/tools/x-color.tools'; +export * from './lib/tools/x-number.tools'; +export * from './lib/tools/x-object.tools'; +export * from './lib/tools/x-string.tools'; +export * from './lib/tools/x-to-form-data'; +export * from './lib/tools/x-formatter.tools'; +export * from './lib/tools/x-date-time.tools'; +export * from './lib/tools/x-exception.tools'; +export * from './lib/tools/x-observable.tools'; +export * from './lib/tools/x-type-detect.tools'; +export * from './lib/tools/x-type-convert.tools'; +export * from './lib/tools/x-type-defaults.tools'; +//#endregion + +// +//#region Common Typings ... +export * from './lib/typings/x.typings'; +export * from './lib/typings/x-page.typings'; +export * from './lib/typings/x-query.typings'; +export * from './lib/typings/x-color.typings'; +export * from './lib/typings/x-storage.typings'; +export * from './lib/typings/x-setting.typings'; +export * from './lib/typings/x-manager.typings'; +export * from './lib/typings/x-date-time.typings'; +export * from './lib/typings/x-exception.typings'; +export * from './lib/typings/x-translation.typings'; +export * from './lib/typings/x-task-manager.typings'; +export * from './lib/typings/x-notification.typings'; +export * from './lib/typings/x-theme-manager.typings'; +export * from './lib/typings/x-base-component.typings'; +//#endregion + +// +//#region Export Directives ... +export * from './lib/directives/x-var.directive'; +export * from './lib/directives/x-color.directive'; +export * from './lib/directives/x-focus.directive'; +export * from './lib/directives/x-render.directive'; +export * from './lib/directives/x-slider.directive'; +export * from './lib/directives/x-on-enter.directive'; +export * from './lib/directives/x-formatter.directive'; +export * from './lib/directives/x-droppable.directive'; +export * from './lib/directives/x-clipboard.directive'; +export * from './lib/directives/x-on-escape.directive'; +export * from './lib/directives/x-file-reader.directive'; +export * from './lib/directives/x-mouse-handler.directive'; +export * from './lib/directives/x-focus-handler.directive'; +export * from './lib/directives/x-scroll-behavior.directive'; +export * from './lib/directives/x-on-outside-click.directive'; +export * from './lib/directives/x-stop-click-propagation.directive'; +//#endregion + +// +//#region Export Validators ... +export * from './lib/validators/x-general-validators'; +export * from './lib/validators/x-valid-url.directive'; +export * from './lib/validators/x-valid-email.directive'; +export * from './lib/validators/x-value-match.directive'; +export * from './lib/validators/x-valid-phone-number.directive'; +//#endregion + +// +//#region Export Pipse ... +export * from './lib/pipes/x-as.pipe'; +export * from './lib/pipes/x-log.pipe'; +export * from './lib/pipes/x-formatter.pipe'; +export * from './lib/pipes/x-safe-html.pipe'; +//#endregion + +// +//#region Tokens ... +export * from './lib/tokens/x-injectable-tokens'; +//#endregion + +// +//#region Export Config ... +export * from './lib/config/x-framework-core.config'; +//#endregion + +// +//#region Module ... +export * from './lib/modules/x-directives.module'; +export * from './lib/x-framework-core.module'; +//#endregion diff --git a/projects/x-framework-core/src/test.ts b/projects/x-framework-core/src/test.ts new file mode 100644 index 0000000..52e5516 --- /dev/null +++ b/projects/x-framework-core/src/test.ts @@ -0,0 +1,26 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js'; +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context(path: string, deep?: boolean, filter?: RegExp): { + keys(): string[]; + (id: string): T; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git a/projects/x-framework-core/tsconfig.lib.ivy.json b/projects/x-framework-core/tsconfig.lib.ivy.json new file mode 100644 index 0000000..571bd13 --- /dev/null +++ b/projects/x-framework-core/tsconfig.lib.ivy.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.lib.json", + "angularCompilerOptions": { + "enableIvy": true + } +} \ No newline at end of file diff --git a/projects/x-framework-core/tsconfig.lib.json b/projects/x-framework-core/tsconfig.lib.json new file mode 100644 index 0000000..1407202 --- /dev/null +++ b/projects/x-framework-core/tsconfig.lib.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/lib", + "target": "es2015", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [], + "lib": [ + "dom", + "es2018" + ] + }, + "exclude": [ + "src/test.ts", + "**/*.spec.ts" + ] +} diff --git a/projects/x-framework-core/tsconfig.lib.prod.json b/projects/x-framework-core/tsconfig.lib.prod.json new file mode 100644 index 0000000..06de549 --- /dev/null +++ b/projects/x-framework-core/tsconfig.lib.prod.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": { + "compilationMode": "partial" + } +} diff --git a/projects/x-framework-core/tsconfig.spec.json b/projects/x-framework-core/tsconfig.spec.json new file mode 100644 index 0000000..715dd0a --- /dev/null +++ b/projects/x-framework-core/tsconfig.spec.json @@ -0,0 +1,17 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "src/test.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ed1ac87 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,39 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "strict": true, + "baseUrl": "./", + "sourceMap": true, + "target": "es2017", + "module": "es2020", + "declaration": false, + "importHelpers": true, + "noImplicitAny": true, + "strictNullChecks": false, + "noImplicitReturns": true, + "outDir": "./dist/out-tsc", + "downlevelIteration": true, + "moduleResolution": "node", + "experimentalDecorators": true, + "noFallthroughCasesInSwitch": true, + "suppressImplicitAnyIndexErrors": true, + "forceConsistentCasingInFileNames": true, + "lib": [ + "es2018", + "dom" + ], + "paths": { + "x-framework-core": [ + "dist/x-framework-core/x-framework-core", + "dist/x-framework-core" + ] + } + }, + "angularCompilerOptions": { + "strictTemplates": true, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "enableI18nLegacyMessageIdFormat": false + } +} \ No newline at end of file