From 4a551d54d1b7803c5dddd9ea92220addb0bfa5c6 Mon Sep 17 00:00:00 2001 From: Hadi Khazaee Asl Date: Sun, 9 Aug 2026 20:36:44 +0330 Subject: [PATCH] Initial Commit on git.saherelm.ir ... --- .editorconfig | 16 + .gitignore | 52 + README.md | 15 + angular.json | 45 + crypto-js.d.ts | 1759 +++++++++++++++++ package.json | 76 + projects/x-framework-services/README.md | 15 + projects/x-framework-services/karma.conf.js | 44 + projects/x-framework-services/ng-package.json | 7 + projects/x-framework-services/package.json | 22 + .../lib/config/x-framework-services.config.ts | 6 + .../x-currency-formatter.directive.ts | 191 ++ .../directives/x-date-presenter.directive.ts | 88 + .../x-from-input-formatter.directive.ts | 493 +++++ .../x-locale-input-formatter.directive.ts | 137 ++ .../lib/directives/x-swipable.directive.ts | 326 +++ .../src/lib/modules/x-directives.module.ts | 44 + .../src/lib/pipes/x-translate.pipe.ts | 55 + .../src/lib/tokens/x-injectable-tokens.ts | 6 + .../src/lib/x-dialog.service.ts | 576 ++++++ .../src/lib/x-framework-services.module.ts | 50 + .../src/lib/x-geolocation.service.ts | 193 ++ .../src/lib/x-history.service.ts | 99 + .../src/lib/x-http-interceptor.service.ts | 86 + .../src/lib/x-identifier.service.ts | 113 ++ .../src/lib/x-localization.service.ts | 290 +++ .../x-manager-base.service.ts | 48 + .../x-manager-final.service.ts | 3 + .../x-manager-route.service.ts | 399 ++++ .../x-manager-ui-state.service.ts | 185 ++ .../src/lib/x-manager.service.ts | 72 + .../src/lib/x-notification.service.ts | 366 ++++ .../src/lib/x-secure-storage.service.ts | 137 ++ .../src/lib/x-security.service.ts | 173 ++ .../src/lib/x-settings.service.ts | 478 +++++ .../src/lib/x-storage.service.ts | 203 ++ .../src/lib/x-task-manager.service.ts | 295 +++ .../src/lib/x-theme-manager.service.ts | 1216 ++++++++++++ .../x-framework-services/src/public-api.ts | 50 + projects/x-framework-services/src/test.ts | 26 + .../tsconfig.lib.ivy.json | 6 + .../x-framework-services/tsconfig.lib.json | 20 + .../tsconfig.lib.prod.json | 10 + .../x-framework-services/tsconfig.spec.json | 17 + tsconfig.json | 39 + 45 files changed, 8547 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 README.md create mode 100644 angular.json create mode 100644 crypto-js.d.ts create mode 100644 package.json create mode 100644 projects/x-framework-services/README.md create mode 100644 projects/x-framework-services/karma.conf.js create mode 100644 projects/x-framework-services/ng-package.json create mode 100644 projects/x-framework-services/package.json create mode 100644 projects/x-framework-services/src/lib/config/x-framework-services.config.ts create mode 100644 projects/x-framework-services/src/lib/directives/x-currency-formatter.directive.ts create mode 100644 projects/x-framework-services/src/lib/directives/x-date-presenter.directive.ts create mode 100644 projects/x-framework-services/src/lib/directives/x-from-input-formatter.directive.ts create mode 100644 projects/x-framework-services/src/lib/directives/x-locale-input-formatter.directive.ts create mode 100644 projects/x-framework-services/src/lib/directives/x-swipable.directive.ts create mode 100644 projects/x-framework-services/src/lib/modules/x-directives.module.ts create mode 100644 projects/x-framework-services/src/lib/pipes/x-translate.pipe.ts create mode 100644 projects/x-framework-services/src/lib/tokens/x-injectable-tokens.ts create mode 100644 projects/x-framework-services/src/lib/x-dialog.service.ts create mode 100644 projects/x-framework-services/src/lib/x-framework-services.module.ts create mode 100644 projects/x-framework-services/src/lib/x-geolocation.service.ts create mode 100644 projects/x-framework-services/src/lib/x-history.service.ts create mode 100644 projects/x-framework-services/src/lib/x-http-interceptor.service.ts create mode 100644 projects/x-framework-services/src/lib/x-identifier.service.ts create mode 100644 projects/x-framework-services/src/lib/x-localization.service.ts create mode 100644 projects/x-framework-services/src/lib/x-manager-service-partial/x-manager-base.service.ts create mode 100644 projects/x-framework-services/src/lib/x-manager-service-partial/x-manager-final.service.ts create mode 100644 projects/x-framework-services/src/lib/x-manager-service-partial/x-manager-route.service.ts create mode 100644 projects/x-framework-services/src/lib/x-manager-service-partial/x-manager-ui-state.service.ts create mode 100644 projects/x-framework-services/src/lib/x-manager.service.ts create mode 100644 projects/x-framework-services/src/lib/x-notification.service.ts create mode 100644 projects/x-framework-services/src/lib/x-secure-storage.service.ts create mode 100644 projects/x-framework-services/src/lib/x-security.service.ts create mode 100644 projects/x-framework-services/src/lib/x-settings.service.ts create mode 100644 projects/x-framework-services/src/lib/x-storage.service.ts create mode 100644 projects/x-framework-services/src/lib/x-task-manager.service.ts create mode 100644 projects/x-framework-services/src/lib/x-theme-manager.service.ts create mode 100644 projects/x-framework-services/src/public-api.ts create mode 100644 projects/x-framework-services/src/test.ts create mode 100644 projects/x-framework-services/tsconfig.lib.ivy.json create mode 100644 projects/x-framework-services/tsconfig.lib.json create mode 100644 projects/x-framework-services/tsconfig.lib.prod.json create mode 100644 projects/x-framework-services/tsconfig.spec.json create mode 100644 tsconfig.json 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..369633c --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# XFrameworkServices + +contains some sort of usefull services, framework controller services, directives based on services and etc. + +most important service in this module is XManagerService which responsible for providing all features of this module in a singe easy to use interface. + +depends on x-framework-core. + +## 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..0626147 --- /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-services": { + "projectType": "library", + "root": "projects/x-framework-services", + "sourceRoot": "projects/x-framework-services/src", + "prefix": "x", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:ng-packagr", + "options": { + "project": "projects/x-framework-services/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "projects/x-framework-services/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "projects/x-framework-services/tsconfig.lib.json" + }, + "ivy": { + "tsConfig": "projects/x-framework-services/tsconfig.lib.ivy.json" + } + }, + "defaultConfiguration": "production" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "main": "projects/x-framework-services/src/test.ts", + "tsConfig": "projects/x-framework-services/tsconfig.spec.json", + "karmaConfig": "projects/x-framework-services/karma.conf.js" + } + } + } + } + }, + "defaultProject": "x-framework-services" +} \ No newline at end of file diff --git a/crypto-js.d.ts b/crypto-js.d.ts new file mode 100644 index 0000000..c21aa9b --- /dev/null +++ b/crypto-js.d.ts @@ -0,0 +1,1759 @@ +// Type definitions for crypto-js v4.0.0 +// Project: https://github.com/brix/crypto-js +// Definitions by: Michael Zabka +// Max Lysenko +// Brendan Early +// Doma +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +type WordArray = CryptoJS.lib.WordArray; +type CipherParams = CryptoJS.lib.CipherParams; +type X64Word = CryptoJS.x64.Word; + +/** + * Encoding strategy. + */ +interface Encoder { + /** + * Converts a word array to a hex string. + * + * @param wordArray The word array. + * + * @return The hex string. + * + * @example + * + * var hexString = CryptoJS.enc.Hex.stringify(wordArray); + */ + stringify(wordArray: WordArray): string; + /** + * Converts a hex string to a word array. + * + * @param hexStr The hex string. + * + * @return The word array. + * + * @example + * + * var wordArray = CryptoJS.enc.Hex.parse(hexString); + */ + parse(str: string): WordArray; +} + +/** + * Abstract buffered block algorithm template. + * + * The property blockSize must be implemented in a concrete subtype. + */ +interface BufferedBlockAlgorithm { + /** + * The number of blocks that should be kept unprocessed in the buffer. Default: 0 + */ + _minBufferSize: number; + /** + * Resets this block algorithm's data buffer to its initial state. + * + * @example + * + * bufferedBlockAlgorithm.reset(); + */ + reset(): void; + /** + * Adds new data to this block algorithm's buffer. + * + * @param data The data to append. Strings are converted to a WordArray using UTF-8. + * + * @example + * + * bufferedBlockAlgorithm._append('data'); + * bufferedBlockAlgorithm._append(wordArray); + */ + _append(data: WordArray | string): void; + /** + * Processes available data blocks. + * + * This method invokes _doProcessBlock(offset), which must be implemented by a concrete subtype. + * + * @param doFlush Whether all blocks and partial blocks should be processed. + * + * @return The processed data. + * + * @example + * + * var processedData = bufferedBlockAlgorithm._process(); + * var processedData = bufferedBlockAlgorithm._process(!!'flush'); + */ + _process(doFlush?: boolean): WordArray; + /** + * Creates a copy of this object. + * + * @return The clone. + * + * @example + * + * var clone = bufferedBlockAlgorithm.clone(); + */ + clone(): BufferedBlockAlgorithm; +} + +/** + * Abstract hasher template. + */ +interface Hasher { + /** + * The number of 32-bit words this hasher operates on. Default: 16 (512 bits) + */ + blockSize: number; + /** + * Resets this hasher to its initial state. + * + * @example + * + * hasher.reset(); + */ + reset(): void; + /** + * Updates this hasher with a message. + * + * @param messageUpdate The message to append. + * + * @return This hasher. + * + * @example + * + * hasher.update('message'); + * hasher.update(wordArray); + */ + update(messageUpdate: WordArray | string): this; + /** + * Finalizes the hash computation. + * Note that the finalize operation is effectively a destructive, read-once operation. + * + * @param messageUpdate (Optional) A final message update. + * + * @return The hash. + * + * @example + * + * var hash = hasher.finalize(); + * var hash = hasher.finalize('message'); + * var hash = hasher.finalize(wordArray); + */ + finalize(messaegUpdate?: WordArray | string): WordArray; +} + +interface HasherStatic { + /** + * Initializes a newly created hasher. + * + * @param cfg (Optional) The configuration options to use for this hash computation. + * + * @example + * + * var hasher = CryptoJS.algo.SHA256.create(); + */ + create(cfg?: object): Hasher; +} + +interface HasherHelper { + (message: WordArray | string, cfg?: object): WordArray; +} + +interface HmacHasherHelper { + (message: WordArray | string, key: WordArray | string): WordArray; +} + +/** + * Abstract base cipher template. + */ +interface Cipher { + /** + * This cipher's key size. Default: 4 (128 bits) + */ + keySize: number; + /** + * This cipher's IV size. Default: 4 (128 bits) + */ + ivSize: number; + /** + * A constant representing encryption mode. + */ + readonly _ENC_XFORM_MODE: number; + /** + * A constant representing decryption mode. + */ + readonly _DEV_XFORM_MODE: number; + + /** + * Resets this cipher to its initial state. + * + * @example + * + * cipher.reset(); + */ + reset(): void; + + /** + * Adds data to be encrypted or decrypted. + * + * @param dataUpdate The data to encrypt or decrypt. + * + * @return The data after processing. + * + * @example + * + * var encrypted = cipher.process('data'); + * var encrypted = cipher.process(wordArray); + */ + process(dataUpdate: WordArray | string): WordArray; + + /** + * Finalizes the encryption or decryption process. + * Note that the finalize operation is effectively a destructive, read-once operation. + * + * @param dataUpdate The final data to encrypt or decrypt. + * + * @return The data after final processing. + * + * @example + * + * var encrypted = cipher.finalize(); + * var encrypted = cipher.finalize('data'); + * var encrypted = cipher.finalize(wordArray); + */ + finalize(dataUpdate?: WordArray | string): WordArray; +} + +interface CipherStatic { + /** + * Creates this cipher in encryption mode. + * + * @param key The key. + * @param cfg (Optional) The configuration options to use for this operation. + * + * @return A cipher instance. + * + * @example + * + * var cipher = CryptoJS.algo.AES.createEncryptor(keyWordArray, { iv: ivWordArray }); + */ + createEncryptor(key: WordArray, cfg?: CipherOption): Cipher; + + /** + * Creates this cipher in decryption mode. + * + * @param key The key. + * @param cfg (Optional) The configuration options to use for this operation. + * + * @return A cipher instance. + * + * @example + * + * var cipher = CryptoJS.algo.AES.createDecryptor(keyWordArray, { iv: ivWordArray }); + */ + createDecryptor(key: WordArray, cfg?: CipherOption): Cipher; + + /** + * Initializes a newly created cipher. + * + * @param xformMode Either the encryption or decryption transormation mode constant. + * @param key The key. + * @param cfg (Optional) The configuration options to use for this operation. + * + * @example + * + * var cipher = CryptoJS.algo.AES.create(CryptoJS.algo.AES._ENC_XFORM_MODE, keyWordArray, { iv: ivWordArray }); + */ + create(xformMode: number, key: WordArray, cfg?: CipherOption): Cipher; +} + +interface CipherHelper { + encrypt( + message: WordArray | string, + key: WordArray | string, + cfg?: CipherOption + ): CipherParams; + decrypt( + ciphertext: CipherParams | string, + key: WordArray | string, + cfg?: CipherOption + ): WordArray; +} + +/** + * Configuration options. + */ +interface CipherOption { + /** + * The IV to use for this operation. + */ + iv?: WordArray | undefined; + format?: Format | undefined; + [key: string]: any; +} + +interface Mode { + /** + * Processes the data block at offset. + * + * @param words The data words to operate on. + * @param offset The offset where the block starts. + * + * @example + * + * mode.processBlock(data.words, offset); + */ + processBlock(words: number[], offset: number): void; +} + +interface ModeStatic { + /** + * Initializes a newly created mode. + * + * @param cipher A block cipher instance. + * @param iv The IV words. + * + * @example + * + * var mode = CryptoJS.mode.CBC.Encryptor.create(cipher, iv.words); + */ + create(cipher: Cipher, iv: number[]): Mode; +} + +/** + * Abstract base block cipher mode template. + */ +interface BlockCipherMode { + Encryptor: ModeStatic; + Decryptor: ModeStatic; + /** + * Creates this mode for encryption. + * + * @param cipher A block cipher instance. + * @param iv The IV words. + * + * @example + * + * var mode = CryptoJS.mode.CBC.createEncryptor(cipher, iv.words); + */ + createEncryptor(cipher: Cipher, iv: number[]): Mode; + + /** + * Creates this mode for decryption. + * + * @param cipher A block cipher instance. + * @param iv The IV words. + * + * @example + * + * var mode = CryptoJS.mode.CBC.createDecryptor(cipher, iv.words); + */ + createDecryptor(cipher: Cipher, iv: number[]): Mode; +} + +/** + * Abstract base block cipher mode template. + */ +interface BlockCipherMode { + /** + * Creates this mode for encryption. + * + * @param cipher A block cipher instance. + * @param iv The IV words. + * + * @example + * + * var mode = CryptoJS.mode.CBC.createEncryptor(cipher, iv.words); + */ + createEncryptor(cipher: Cipher): Mode; +} + +/** + * Padding strategy. + */ +interface Padding { + /** + * Pads data using the algorithm defined in PKCS #5/7. + * + * @param data The data to pad. + * @param blockSize The multiple that the data should be padded to. + * + * @example + * + * CryptoJS.pad.Pkcs7.pad(wordArray, 4); + */ + pad(data: WordArray, blockSize: number): void; + + /** + * Unpads data that had been padded using the algorithm defined in PKCS #5/7. + * + * @param data The data to unpad. + * + * @example + * + * CryptoJS.pad.Pkcs7.unpad(wordArray); + */ + unpad(data: WordArray): void; +} + +/** + * Abstract base block cipher template. + */ +interface BlockCipher { + /** + * The number of 32-bit words this cipher operates on. Default: 4 (128 bits) + */ + blockSize: number; +} + +/** + * Configuration options. + */ +interface BlockCipherOption { + /** + * The block mode to use. Default: CBC + */ + mode: Mode; + /** + * The padding strategy to use. Default: Pkcs7 + */ + padding: Padding; +} +/** + * Formatting strategy. + */ +interface Format { + /** + * Converts a cipher params object to an OpenSSL-compatible string. + * + * @param cipherParams The cipher params object. + * + * @return The OpenSSL-compatible string. + * + * @example + * + * var openSSLString = CryptoJS.format.OpenSSL.stringify(cipherParams); + */ + stringify(cipherParams: CipherParams): string; + + /** + * Converts an OpenSSL-compatible string to a cipher params object. + * + * @param openSSLStr The OpenSSL-compatible string. + * + * @return The cipher params object. + * + * @example + * + * var cipherParams = CryptoJS.format.OpenSSL.parse(openSSLString); + */ + parse(str: string): CipherParams; +} + +/** + * An array of 64-bit words. + */ +interface X64WordArray { + /** + * The array of CryptoJS.x64.Word objects. + */ + words: number[]; + /** + * The number of significant bytes in this word array. + */ + sigBytes: number; + + /** + * Converts this 64-bit word array to a 32-bit word array. + * + * @return This word array's data as a 32-bit word array. + * + * @example + * + * var x32WordArray = x64WordArray.toX32(); + */ + toX32(): WordArray; + + /** + * Creates a copy of this word array. + * + * @return The clone. + * + * @example + * + * var clone = x64WordArray.clone(); + */ + clone(): X64WordArray; +} + +/** + * Base object for prototypal inheritance. + */ +interface Base { + /** + * Creates a copy of this object. + * + * @return The clone. + * + * @example + * + * var clone = instance.clone(); + */ + clone(): this; +} + +/** + * Configuration options. + */ +interface KDFOption { + /** + * The key size in words to generate. + */ + keySize?: number | undefined; + /** + * The hasher to use. + */ + hasher?: HasherStatic | undefined; + /** + * The number of iterations to perform. + */ + iterations?: number | undefined; +} + +declare namespace CryptoJS { + /** + * Library namespace. + */ + export namespace lib { + /** + * Base object for prototypal inheritance. + */ + const Base: { + /** + * Creates a new object that inherits from this object. + * + * @param overrides Properties to copy into the new object. + * + * @return The new object. + * + * @example + * + * var MyType = CryptoJS.lib.Base.extend({ + * field: 'value', + * + * method: function () { + * } + * }); + */ + extend(overrides: object): any; + + /** + * Extends this object and runs the init method. + * Arguments to create() will be passed to init(). + * + * @return The new object. + * + * @example + * + * var instance = MyType.create(); + */ + create(...args: any[]): any; + + /** + * Copies properties into this object. + * + * @param properties The properties to mix in. + * + * @example + * + * MyType.mixIn({ + * field: 'value' + * }); + */ + mixIn(properties: object): any; + }; + + /** + * An array of 32-bit words. + */ + interface WordArray { + /** + * The array of 32-bit words. + */ + words: number[]; + /** + * The number of significant bytes in this word array. + */ + sigBytes: number; + /** + * Converts this word array to a string. + * + * @param encoder (Optional) The encoding strategy to use. Default: CryptoJS.enc.Hex + * + * @return The stringified word array. + * + * @example + * + * var string = wordArray + ''; + * var string = wordArray.toString(); + * var string = wordArray.toString(CryptoJS.enc.Utf8); + */ + toString(encoder?: Encoder): string; + + /** + * Concatenates a word array to this word array. + * + * @param wordArray The word array to append. + * + * @return This word array. + * + * @example + * + * wordArray1.concat(wordArray2); + */ + concat(wordArray: WordArray): this; + + /** + * Removes insignificant bits. + * + * @example + * + * wordArray.clamp(); + */ + clamp(): void; + + /** + * Creates a copy of this word array. + * + * @return The clone. + * + * @example + * + * var clone = wordArray.clone(); + */ + clone(): WordArray; + } + const WordArray: { + /** + * Initializes a newly created word array. + * + * @param words (Optional) An array of 32-bit words. + * @param sigBytes (Optional) The number of significant bytes in the words. + * + * @example + * + * var wordArray = CryptoJS.lib.WordArray.create(); + * var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607]); + * var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607], 6); + */ + create(words?: number[], sigBytes?: number): WordArray; + /** + * Creates a word array filled with random bytes. + * + * @param nBytes The number of random bytes to generate. + * + * @return The random word array. + * + * @example + * + * var wordArray = CryptoJS.lib.WordArray.random(16); + */ + random(nBytes: number): WordArray; + }; + + const BufferedBlockAlgorithm: any; + + const Hasher: { + /** + * Creates a shortcut function to a hasher's object interface. + * + * @param hasher The hasher to create a helper for. + * + * @return The shortcut function. + * + * @example + * + * var SHA256 = CryptoJS.lib.Hasher._createHelper(CryptoJS.algo.SHA256); + */ + _createHelper(hasher: HasherStatic): HasherHelper; + /** + * Creates a shortcut function to the HMAC's object interface. + * + * @param hasher The hasher to use in this HMAC helper. + * + * @return The shortcut function. + * + * @example + * + * var HmacSHA256 = CryptoJS.lib.Hasher._createHmacHelper(CryptoJS.algo.SHA256); + */ + _createHmacHelper(hasher: HasherStatic): HmacHasherHelper; + }; + + const Cipher: { + /** + * Creates shortcut functions to a cipher's object interface. + * + * @param cipher The cipher to create a helper for. + * + * @return An object with encrypt and decrypt shortcut functions. + * + * @example + * + * var AES = CryptoJS.lib.Cipher._createHelper(CryptoJS.algo.AES); + */ + _createHelper(cipher: Cipher): CipherHelper; + }; + + interface CipherOption { + /** + * The IV to use for this operation. + */ + iv?: WordArray | undefined; + format?: Format | undefined; + [key: string]: any; + } + + /** + * A collection of cipher parameters. + */ + interface CipherParams { + /** + * The raw ciphertext. + */ + ciphertext: WordArray; + /** + * The key to this ciphertext. + */ + key: WordArray; + /** + * The IV used in the ciphering operation. + */ + iv: WordArray; + /** + * The salt used with a key derivation function. + */ + salt: WordArray; + /** + * The cipher algorithm. + */ + algorithm: CipherStatic; + /** + * The block mode used in the ciphering operation. + */ + mode: Mode; + /** + * The padding scheme used in the ciphering operation. + */ + padding: Padding; + /** + * The block size of the cipher. + */ + blockSize: number; + /** + * The default formatting strategy to convert this cipher params object to a string. + */ + formatter: Format; + /** + * Converts this cipher params object to a string. + * + * @param formatter (Optional) The formatting strategy to use. + * + * @return The stringified cipher params. + * + * @throws Error If neither the formatter nor the default formatter is set. + * + * @example + * + * var string = cipherParams + ''; + * var string = cipherParams.toString(); + * var string = cipherParams.toString(CryptoJS.format.OpenSSL); + */ + toString(formatter?: Format): string; + } + const CipherParams: { + /** + * Initializes a newly created cipher params object. + * + * @param cipherParams An object with any of the possible cipher parameters. + * + * @example + * + * var cipherParams = CryptoJS.lib.CipherParams.create({ + * ciphertext: ciphertextWordArray, + * key: keyWordArray, + * iv: ivWordArray, + * salt: saltWordArray, + * algorithm: CryptoJS.algo.AES, + * mode: CryptoJS.mode.CBC, + * padding: CryptoJS.pad.PKCS7, + * blockSize: 4, + * formatter: CryptoJS.format.OpenSSL + * }); + */ + create(cipherParams: Partial): CipherParams; + }; + + /** + * Abstract base stream cipher template. + */ + interface StreamCipher extends Cipher { + /** + * The number of 32-bit words this cipher operates on. Default: 1 (32 bits) + */ + blockSize: number; + } + + /** + * Abstract base block cipher mode template. + */ + const BlockCipherMode: any; + + /** + * A cipher wrapper that returns ciphertext as a serializable cipher params object. + */ + const SerializableCipher: { + /** + * Encrypts a message. + * + * @param cipher The cipher algorithm to use. + * @param message The message to encrypt. + * @param key The key. + * @param cfg (Optional) The configuration options to use for this operation. + * + * @return A cipher params object. + * + * @example + * + * var ciphertextParams = CryptoJS.lib.SerializableCipher.encrypt(CryptoJS.algo.AES, message, key); + * var ciphertextParams = CryptoJS.lib.SerializableCipher.encrypt(CryptoJS.algo.AES, message, key, { iv: iv }); + * var ciphertextParams = CryptoJS.lib.SerializableCipher.encrypt(CryptoJS.algo.AES, message, key, { iv: iv, format: CryptoJS.format.OpenSSL }); + */ + encrypt( + cipher: CipherStatic, + message: WordArray | string, + key: WordArray, + cfg?: CipherOption + ): CipherParams; + + /** + * Decrypts serialized ciphertext. + * + * @param cipher The cipher algorithm to use. + * @param ciphertext The ciphertext to decrypt. + * @param key The key. + * @param cfg (Optional) The configuration options to use for this operation. + * + * @return The plaintext. + * + * @example + * + * var plaintext = CryptoJS.lib.SerializableCipher.decrypt(CryptoJS.algo.AES, formattedCiphertext, key, { iv: iv, format: CryptoJS.format.OpenSSL }); + * var plaintext = CryptoJS.lib.SerializableCipher.decrypt(CryptoJS.algo.AES, ciphertextParams, key, { iv: iv, format: CryptoJS.format.OpenSSL }); + */ + decrypt( + cipher: CipherStatic, + ciphertext: WordArray | string, + key: WordArray, + cfg?: CipherOption + ): CipherParams; + + /** + * Converts serialized ciphertext to CipherParams, + * else assumed CipherParams already and returns ciphertext unchanged. + * + * @param ciphertext The ciphertext. + * @param format The formatting strategy to use to parse serialized ciphertext. + * + * @return The unserialized ciphertext. + * + * @example + * + * var ciphertextParams = CryptoJS.lib.SerializableCipher._parse(ciphertextStringOrParams, format); + */ + _parse(ciphertext: CipherParams | string, format: Format): CipherParams; + }; + + /** + * A serializable cipher wrapper that derives the key from a password, + * and returns ciphertext as a serializable cipher params object. + */ + const PasswordBasedCipher: { + /** + * Encrypts a message using a password. + * + * @param cipher The cipher algorithm to use. + * @param message The message to encrypt. + * @param password The password. + * @param cfg (Optional) The configuration options to use for this operation. + * + * @return A cipher params object. + * + * @example + * + * var ciphertextParams = CryptoJS.lib.PasswordBasedCipher.encrypt(CryptoJS.algo.AES, message, 'password'); + * var ciphertextParams = CryptoJS.lib.PasswordBasedCipher.encrypt(CryptoJS.algo.AES, message, 'password', { format: CryptoJS.format.OpenSSL }); + */ + encrypt( + cipher: CipherStatic, + message: WordArray | string, + password: string, + cfg?: CipherOption + ): CipherParams; + + /** + * Decrypts serialized ciphertext using a password. + * + * @param cipher The cipher algorithm to use. + * @param ciphertext The ciphertext to decrypt. + * @param password The password. + * @param cfg (Optional) The configuration options to use for this operation. + * + * @return The plaintext. + * + * @example + * + * var plaintext = CryptoJS.lib.PasswordBasedCipher.decrypt(CryptoJS.algo.AES, formattedCiphertext, 'password', { format: CryptoJS.format.OpenSSL }); + * var plaintext = CryptoJS.lib.PasswordBasedCipher.decrypt(CryptoJS.algo.AES, ciphertextParams, 'password', { format: CryptoJS.format.OpenSSL }); + */ + decrypt( + cipher: CipherStatic, + ciphertext: CipherParams | string, + password: string, + cfg?: CipherOption + ): WordArray; + }; + } + /** + * Padding namespace. + */ + export namespace pad { + /** + * PKCS #5/7 padding strategy. + */ + const Pkcs7: Padding; + + /** + * ANSI X.923 padding strategy. + */ + const AnsiX923: Padding; + + /** + * ISO 10126 padding strategy. + */ + const Iso10126: Padding; + + /** + * ISO/IEC 9797-1 Padding Method 2. + */ + const Iso97971: Padding; + + /** + * Zero padding strategy. + */ + const ZeroPadding: Padding; + + /** + * A noop padding strategy. + */ + const NoPadding: Padding; + } + + /** + * Key derivation function namespace. + */ + export namespace kdf { + /** + * OpenSSL key derivation function. + */ + const OpenSSL: { + /** + * Derives a key and IV from a password. + * + * @param password The password to derive from. + * @param keySize The size in words of the key to generate. + * @param ivSize The size in words of the IV to generate. + * @param salt (Optional) A 64-bit salt to use. If omitted, a salt will be generated randomly. + * + * @return A cipher params object with the key, IV, and salt. + * + * @example + * + * var derivedParams = CryptoJS.kdf.OpenSSL.execute('Password', 256/32, 128/32); + * var derivedParams = CryptoJS.kdf.OpenSSL.execute('Password', 256/32, 128/32, 'saltsalt'); + */ + execute( + password: string, + keySize: number, + ivSize: number, + salt?: WordArray | string + ): CipherParams; + }; + } + + /** + * Mode namespace. + */ + export namespace mode { + /** + * Cipher Block Chaining mode. + */ + const CBC: BlockCipherMode; + + /** + * Cipher Feedback block mode. + */ + const CFB: BlockCipherMode; + /** + * Counter block mode. + */ + const CTR: BlockCipherMode; + /** + * @preserve + * Counter block mode compatible with Dr Brian Gladman fileenc.c + * derived from CryptoJS.mode.CTR + * Jan Hruby jhruby.web@gmail.com + */ + const CTRGladman: BlockCipherMode; + /** + * Output Feedback block mode. + */ + const OFB: BlockCipherMode; + + /** + * Electronic Codebook block mode. + */ + const ECB: BlockCipherMode; + } + + /** + * Format namespace. + */ + export namespace format { + /** + * OpenSSL formatting strategy. + */ + const OpenSSL: Format; + const Hex: Format; + } + + /** + * Encoder namespace. + */ + export namespace enc { + /** + * Hex encoding strategy. + */ + const Hex: Encoder; + /** + * Latin1 encoding strategy. + */ + const Latin1: Encoder; + /** + * UTF-8 encoding strategy. + */ + const Utf8: Encoder; + /** + * UTF-16 BE encoding strategy. + */ + const Utf16: Encoder; + const Utf16BE: Encoder; + + /** + * UTF-16 LE encoding strategy. + */ + const Utf16LE: Encoder; + /** + * Base64 encoding strategy. + */ + const Base64: Encoder; + } + + /** + * Algorithm namespace. + */ + export namespace algo { + /** + * MD5 hash algorithm. + */ + const MD5: HasherStatic; + + /** + * SHA-1 hash algorithm. + */ + const SHA1: HasherStatic; + + /** + * SHA-256 hash algorithm. + */ + const SHA256: HasherStatic; + /** + * SHA-224 hash algorithm. + */ + const SHA224: HasherStatic; + /** + * SHA-512 hash algorithm. + */ + const SHA512: HasherStatic; + + /** + * SHA-384 hash algorithm. + */ + const SHA384: HasherStatic; + /** + * SHA-3 hash algorithm. + */ + const SHA3: HasherStatic; + /** + * RIPEMD160 hash algorithm. + */ + const RIPEMD160: HasherStatic; + /** + * HMAC algorithm. + */ + abstract class HMAC { + /** + * Initializes a newly created HMAC. + * + * @param hasher The hash algorithm to use. + * @param key The secret key. + * + * @example + * + * var hmacHasher = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256, key); + */ + static create(hasher: HasherStatic, key: WordArray | string): HMAC; + /** + * Resets this HMAC to its initial state. + * + * @example + * + * hmacHasher.reset(); + */ + reset(): void; + + /** + * Updates this HMAC with a message. + * + * @param messageUpdate The message to append. + * + * @return This HMAC instance. + * + * @example + * + * hmacHasher.update('message'); + * hmacHasher.update(wordArray); + */ + update(messageUpdate: WordArray | string): this; + + /** + * Finalizes the HMAC computation. + * Note that the finalize operation is effectively a destructive, read-once operation. + * + * @param messageUpdate (Optional) A final message update. + * + * @return The HMAC. + * + * @example + * + * var hmac = hmacHasher.finalize(); + * var hmac = hmacHasher.finalize('message'); + * var hmac = hmacHasher.finalize(wordArray); + */ + finalize(messageUpdate?: WordArray | string): WordArray; + } + /** + * Password-Based Key Derivation Function 2 algorithm. + */ + abstract class PBKDF2 { + /** + * Initializes a newly created key derivation function. + * + * @param cfg (Optional) The configuration options to use for the derivation. + * + * @example + * + * var kdf = CryptoJS.algo.PBKDF2.create(); + * var kdf = CryptoJS.algo.PBKDF2.create({ keySize: 8 }); + * var kdf = CryptoJS.algo.PBKDF2.create({ keySize: 8, iterations: 1000 }); + */ + static create(cfg?: KDFOption): PBKDF2; + + /** + * Computes the Password-Based Key Derivation Function 2. + * + * @param password The password. + * @param salt A salt. + * + * @return The derived key. + * + * @example + * + * var key = kdf.compute(password, salt); + */ + compute(password: WordArray | string, salt: WordArray): WordArray; + } + /** + * This key derivation function is meant to conform with EVP_BytesToKey. + * www.openssl.org/docs/crypto/EVP_BytesToKey.html + */ + abstract class EvpKDF { + /** + * Initializes a newly created key derivation function. + * + * @param cfg (Optional) The configuration options to use for the derivation. + * + * @example + * + * var kdf = CryptoJS.algo.EvpKDF.create(); + * var kdf = CryptoJS.algo.EvpKDF.create({ keySize: 8 }); + * var kdf = CryptoJS.algo.EvpKDF.create({ keySize: 8, iterations: 1000 }); + */ + static create(cfg?: { + keySize: number; + hasher?: HasherStatic | undefined; + iterations: number; + }): EvpKDF; + + /** + * Derives a key from a password. + * + * @param password The password. + * @param salt A salt. + * + * @return The derived key. + * + * @example + * + * var key = kdf.compute(password, salt); + */ + compute(password: WordArray | string, salt: WordArray): WordArray; + } + + /** + * AES block cipher algorithm. + */ + const AES: CipherStatic; + + /** + * DES block cipher algorithm. + */ + const DES: CipherStatic; + + /** + * Triple-DES block cipher algorithm. + */ + const TripleDES: CipherStatic; + + /** + * RC4 stream cipher algorithm. + */ + const RC4: CipherStatic; + + /** + * Modified RC4 stream cipher algorithm. + */ + const RC4Drop: CipherStatic; + + /** + * Rabbit stream cipher algorithm + */ + const Rabbit: CipherStatic; + + /** + * Rabbit stream cipher algorithm. + * + * This is a legacy version that neglected to convert the key to little-endian. + * This error doesn't affect the cipher's security, + * but it does affect its compatibility with other implementations. + */ + const RabbitLegacy: CipherStatic; + } + + /** + * x64 namespace. + */ + export namespace x64 { + /** + * A 64-bit word. + */ + interface Word { + /** + * Bitwise NOTs this word. + * + * @return A new x64-Word object after negating. + * + * @example + * + * var negated = x64Word.not(); + */ + not(): X64Word; + /** + * Bitwise ANDs this word with the passed word. + * + * @param word The x64-Word to AND with this word. + * + * @return A new x64-Word object after ANDing. + * + * @example + * + * var anded = x64Word.and(anotherX64Word); + */ + and(word: X64Word): X64Word; + + /** + * Bitwise ORs this word with the passed word. + * + * @param word The x64-Word to OR with this word. + * + * @return A new x64-Word object after ORing. + * + * @example + * + * var ored = x64Word.or(anotherX64Word); + */ + or(word: X64Word): X64Word; + + /** + * Bitwise XORs this word with the passed word. + * + * @param word The x64-Word to XOR with this word. + * + * @return A new x64-Word object after XORing. + * + * @example + * + * var xored = x64Word.xor(anotherX64Word); + */ + xor(word: X64Word): X64Word; + /** + * Shifts this word n bits to the left. + * + * @param n The number of bits to shift. + * + * @return A new x64-Word object after shifting. + * + * @example + * + * var shifted = x64Word.shiftL(25); + */ + shiftL(n: number): X64Word; + /** + * Shifts this word n bits to the right. + * + * @param n The number of bits to shift. + * + * @return A new x64-Word object after shifting. + * + * @example + * + * var shifted = x64Word.shiftR(7); + */ + shiftR(n: number): X64Word; + /** + * Rotates this word n bits to the left. + * + * @param n The number of bits to rotate. + * + * @return A new x64-Word object after rotating. + * + * @example + * + * var rotated = x64Word.rotL(25); + */ + rotL(n: number): X64Word; + + /** + * Rotates this word n bits to the right. + * + * @param n The number of bits to rotate. + * + * @return A new x64-Word object after rotating. + * + * @example + * + * var rotated = x64Word.rotR(7); + */ + rotR(n: number): X64Word; + /** + * Adds this word with the passed word. + * + * @param word The x64-Word to add with this word. + * + * @return A new x64-Word object after adding. + * + * @example + * + * var added = x64Word.add(anotherX64Word); + */ + add(word: X64Word): X64Word; + } + + const Word: { + /** + * Initializes a newly created 64-bit word. + * + * @param high The high 32 bits. + * @param low The low 32 bits. + * + * @example + * + * var x64Word = CryptoJS.x64.Word.create(0x00010203, 0x04050607); + */ + create(high: number, low: number): X64Word; + }; + + /** + * Initializes a newly created word array. + * + * @param words (Optional) An array of CryptoJS.x64.Word objects. + * @param sigBytes (Optional) The number of significant bytes in the words. + * + * @example + * + * var wordArray = CryptoJS.x64.WordArray.create(); + * + * var wordArray = CryptoJS.x64.WordArray.create([ + * CryptoJS.x64.Word.create(0x00010203, 0x04050607), + * CryptoJS.x64.Word.create(0x18191a1b, 0x1c1d1e1f) + * ]); + * + * var wordArray = CryptoJS.x64.WordArray.create([ + * CryptoJS.x64.Word.create(0x00010203, 0x04050607), + * CryptoJS.x64.Word.create(0x18191a1b, 0x1c1d1e1f) + * ], 10); + */ + const WordArray: { + create(words?: X64WordArray[], sigBytes?: number): X64WordArray; + }; + } + + /** + * Shortcut function to the hasher's object interface. + * + * @param message The message to hash. + * + * @return The hash. + * + * @example + * + * var hash = CryptoJS.MD5('message'); + * var hash = CryptoJS.MD5(wordArray); + */ + export const MD5: HasherHelper; + /** + * Shortcut function to the HMAC's object interface. + * + * @param message The message to hash. + * @param key The secret key. + * + * @return The HMA'C. + * + * @example + * + * var hmac = CryptoJS.HmacMD5(message, key); + */ + export const HmacMD5: HmacHasherHelper; + /** + * Shortcut function to the hasher's object interface. + * + * @param message The message to hash. + * + * @return The hash. + * + * @example + * + * var hash = CryptoJS.SHA1('message'); + * var hash = CryptoJS.SHA1(wordArray); + */ + export const SHA1: HasherHelper; + /** + * Shortcut function to the HMAC's object interface. + * + * @param message The message to hash. + * @param key The secret key. + * + * @return The HMAC. + * + * @example + * + * var hmac = CryptoJS.HmacSHA1(message, key); + */ + export const HmacSHA1: HmacHasherHelper; + + /** + * Shortcut function to the hasher's object interface. + * + * @param message The message to hash. + * + * @return The hash. + * + * @example + * + * var hash = CryptoJS.SHA256('message'); + * var hash = CryptoJS.SHA256(wordArray); + */ + export const SHA256: HasherHelper; + /** + * Shortcut function to the HMAC's object interface. + * + * @param message The message to hash. + * @param key The secret key. + * + * @return The HMAC. + * + * @example + * + * var hmac = CryptoJS.HmacSHA256(message, key); + */ + export const HmacSHA256: HmacHasherHelper; + /** + * Shortcut function to the hasher's object interface. + * + * @param message The message to hash. + * + * @return The hash. + * + * @example + * + * var hash = CryptoJS.SHA224('message'); + * var hash = CryptoJS.SHA224(wordArray); + */ + export const SHA224: HasherHelper; + /** + * Shortcut function to the HMAC's object interface. + * + * @param message The message to hash. + * @param key The secret key. + * + * @return The HMAC. + * + * @example + * + * var hmac = CryptoJS.HmacSHA224(message, key); + */ + export const HmacSHA224: HmacHasherHelper; + /** + * Shortcut function to the hasher's object interface. + * + * @param message The message to hash. + * + * @return The hash. + * + * @example + * + * var hash = CryptoJS.SHA512('message'); + * var hash = CryptoJS.SHA512(wordArray); + */ + export const SHA512: HasherHelper; + /** + * Shortcut function to the HMAC's object interface. + * + * @param message The message to hash. + * @param key The secret key. + * + * @return The HMAC. + * + * @example + * + * var hmac = CryptoJS.HmacSHA512(message, key); + */ + export const HmacSHA512: HmacHasherHelper; + /** + * Shortcut function to the hasher's object interface. + * + * @param message The message to hash. + * + * @return The hash. + * + * @example + * + * var hash = CryptoJS.SHA384('message'); + * var hash = CryptoJS.SHA384(wordArray); + */ + export const SHA384: HasherHelper; + /** + * Shortcut function to the HMAC's object interface. + * + * @param message The message to hash. + * @param key The secret key. + * + * @return The HMAC. + * + * @example + * + * var hmac = CryptoJS.HmacSHA384(message, key); + */ + export const HmacSHA384: HmacHasherHelper; + + /** + * Shortcut function to the hasher's object interface. + * + * @param message The message to hash. + * + * @return The hash. + * + * @example + * + * var hash = CryptoJS.SHA3('message'); + * var hash = CryptoJS.SHA3(wordArray); + */ + export const SHA3: HasherHelper; + /** + * Shortcut function to the HMAC's object interface. + * + * @param message The message to hash. + * @param key The secret key. + * + * @return The HMAC. + * + * @example + * + * var hmac = CryptoJS.HmacSHA3(message, key); + */ + export const HmacSHA3: HmacHasherHelper; + + /** + * Shortcut function to the hasher's object interface. + * + * @param message The message to hash. + * + * @return The hash. + * + * @example + * + * var hash = CryptoJS.RIPEMD160('message'); + * var hash = CryptoJS.RIPEMD160(wordArray); + */ + export const RIPEMD160: HasherHelper; + /** + * Shortcut function to the HMAC's object interface. + * + * @param message The message to hash. + * @param key The secret key. + * + * @return The HMAC. + * + * @example + * + * var hmac = CryptoJS.HmacRIPEMD160(message, key); + */ + export const HmacRIPEMD160: HmacHasherHelper; + /** + * Computes the Password-Based Key Derivation Function 2. + * + * @param password The password. + * @param salt A salt. + * @param cfg (Optional) The configuration options to use for this computation. + * + * @return The derived key. + * + * @example + * + * var key = CryptoJS.PBKDF2(password, salt); + * var key = CryptoJS.PBKDF2(password, salt, { keySize: 8 }); + * var key = CryptoJS.PBKDF2(password, salt, { keySize: 8, iterations: 1000 }); + */ + export function PBKDF2( + password: WordArray | string, + salt: WordArray | string, + cfg?: KDFOption + ): WordArray; + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.AES.encrypt(message, key, cfg); + * var plaintext = CryptoJS.AES.decrypt(ciphertext, key, cfg); + */ + export const AES: CipherHelper; + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.DES.encrypt(message, key, cfg); + * var plaintext = CryptoJS.DES.decrypt(ciphertext, key, cfg); + */ + export const DES: CipherHelper; + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.TripleDES.encrypt(message, key, cfg); + * var plaintext = CryptoJS.TripleDES.decrypt(ciphertext, key, cfg); + */ + export const TripleDES: CipherHelper; + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.RC4.encrypt(message, key, cfg); + * var plaintext = CryptoJS.RC4.decrypt(ciphertext, key, cfg); + */ + export const RC4: CipherHelper; + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.RC4Drop.encrypt(message, key, cfg); + * var plaintext = CryptoJS.RC4Drop.decrypt(ciphertext, key, cfg); + */ + export const RC4Drop: CipherHelper; + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.Rabbit.encrypt(message, key, cfg); + * var plaintext = CryptoJS.Rabbit.decrypt(ciphertext, key, cfg); + */ + export const Rabbit: CipherHelper; + + /** + * Shortcut functions to the cipher's object interface. + * + * @example + * + * var ciphertext = CryptoJS.RabbitLegacy.encrypt(message, key, cfg); + * var plaintext = CryptoJS.RabbitLegacy.decrypt(ciphertext, key, cfg); + */ + export const RabbitLegacy: CipherHelper; + + /** + * Derives a key from a password. + * + * @param password The password. + * @param salt A salt. + * @param cfg (Optional) The configuration options to use for this computation. + * + * @return The derived key. + * + * @example + * + * var key = CryptoJS.EvpKDF(password, salt); + * var key = CryptoJS.EvpKDF(password, salt, { keySize: 8 }); + * var key = CryptoJS.EvpKDF(password, salt, { keySize: 8, iterations: 1000 }); + */ + export function EvpKDF( + password: WordArray | string, + salt: WordArray | string, + cfg?: { + keySize: number; + hasher?: HasherStatic | undefined; + iterations: number; + } + ): WordArray; +} + +export = CryptoJS; diff --git a/package.json b/package.json new file mode 100644 index 0000000..cf66c6e --- /dev/null +++ b/package.json @@ -0,0 +1,76 @@ +{ + "name": "x-framework-services-holder", + "version": "0.0.2", + "description": "an isolation layoer of xFrameworkServices module", + "keywords": [ + "x-framework", + "x-framework-services" + ], + "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-services --configuration=ivy", + "serveDocs": "compodoc -p projects/x-framework-services/tsconfig.lib.json --theme material -s", + "buildModule": "npm run preBuildModule && ng build x-framework-services --configuration production", + "packModule": "cd dist/x-framework-services && npm pack && shx mv *.tgz ../../../../pack && cd ../..", + "prepareDependenciesByLink": "npm link x-framework-core", + "prepareDependencies": "npm i --save-dev ../../pack/x-framework-core-0.0.1.tgz", + "prepareDependenciesFromRegistry": "npm i -D --registry https://npm.saherelmhub.ir x-framework-core", + "preBuildModule": "shx true --silent mkdir ../../pack && shx true --silent rm -rf dist && shx true --silent rm ../../pack/x-framework-services*.tgz && npm run prepareDependencies", + "generateDocs": "compodoc -p projects/x-framework-services/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/crypto-js": "^4.0.2", + "@types/howler": "^2.2.3", + "@types/jasmine": "~3.6.0", + "@types/node": "^12.11.1", + "@types/uuid": "^8.3.1", + "crypto-js": "^4.0.0", + "howler": "^2.2.3", + "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", + "ngx-device-detector": "^2.1.1", + "shx": "^0.3.3", + "typescript": "~4.2.3", + "uuid": "^8.3.2", + "x-framework-core": "file:../../pack/x-framework-core-0.0.1.tgz" + } +} diff --git a/projects/x-framework-services/README.md b/projects/x-framework-services/README.md new file mode 100644 index 0000000..70bfe82 --- /dev/null +++ b/projects/x-framework-services/README.md @@ -0,0 +1,15 @@ +# XFrameworkServices + +contains some sort of usefull services, framework controller services, directives based on services and etc. + +most important service in this module is XManagerService which responsible for providing all features of this module in a singe easy to use interface. + +depends on x-framework-core. + +## Maintainer + +Hadi Khazaee asl + + + +hadi_khazaee_asl@yahoo.com diff --git a/projects/x-framework-services/karma.conf.js b/projects/x-framework-services/karma.conf.js new file mode 100644 index 0000000..506b023 --- /dev/null +++ b/projects/x-framework-services/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-services'), + 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-services/ng-package.json b/projects/x-framework-services/ng-package.json new file mode 100644 index 0000000..91fc442 --- /dev/null +++ b/projects/x-framework-services/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../dist/x-framework-services", + "lib": { + "entryFile": "src/public-api.ts" + } +} \ No newline at end of file diff --git a/projects/x-framework-services/package.json b/projects/x-framework-services/package.json new file mode 100644 index 0000000..e3278c9 --- /dev/null +++ b/projects/x-framework-services/package.json @@ -0,0 +1,22 @@ +{ + "name": "x-framework-services", + "version": "0.0.1", + "description": "provide services of xFramework", + "keywords": [ + "x-framework", + "x-framework-services" + ], + "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" + } +} \ No newline at end of file diff --git a/projects/x-framework-services/src/lib/config/x-framework-services.config.ts b/projects/x-framework-services/src/lib/config/x-framework-services.config.ts new file mode 100644 index 0000000..ab3803d --- /dev/null +++ b/projects/x-framework-services/src/lib/config/x-framework-services.config.ts @@ -0,0 +1,6 @@ +import { XFrameworkCoreConfig } from 'x-framework-core'; + +// tslint:disable-next-line:no-empty-interface +export interface XFrameworkServicesConfig extends XFrameworkCoreConfig { + geoLocationConfig?: PositionOptions +} diff --git a/projects/x-framework-services/src/lib/directives/x-currency-formatter.directive.ts b/projects/x-framework-services/src/lib/directives/x-currency-formatter.directive.ts new file mode 100644 index 0000000..e983b07 --- /dev/null +++ b/projects/x-framework-services/src/lib/directives/x-currency-formatter.directive.ts @@ -0,0 +1,191 @@ +import { + Input, + Inject, + Renderer2, + Directive, + ElementRef, + forwardRef, + HostListener +} from '@angular/core'; +import { + XLocale, + removeAll, + applyLocale, + isNullOrEmptyString +} from 'x-framework-core'; +import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'; +import { X_FRAMEWORK_SERVICES_CONFIG } from '../tokens/x-injectable-tokens'; +import { XFrameworkServicesConfig } from '../config/x-framework-services.config'; + +export const X_CURRENCY_FORMATTER: any = { + useExisting: forwardRef(() => XCurrencyFormatterDirective), + provide: NG_VALUE_ACCESSOR, + multi: true +}; + +@Directive({ + // tslint:disable-next-line:directive-selector + selector: '[xCurrencyFormatter]', + providers: [X_CURRENCY_FORMATTER] +}) +export class XCurrencyFormatterDirective implements ControlValueAccessor { + // + //#region Props ... + @Input() + locale: XLocale; + + @Input() + applyLocaleToCurrency: boolean; + + @Input() + currencySeperator: string; + + @Input() + enabledCurrencyFormatter: boolean; + + onChange = (_: any) => {}; + + onTouched = () => {}; + //#endregion + + // + //#region Constructor ... + constructor( + private renderer: Renderer2, + private elementRef: ElementRef, + @Inject(X_FRAMEWORK_SERVICES_CONFIG) + private config: XFrameworkServicesConfig + ) {} + //#endregion + + // + //#region Host Bindings ... + @HostListener('change', ['$event.target.value']) + change(value: string) { + // + const res = this.getUnformattedValue(value); + this.onChange(res); + } + + @HostListener('input', ['$event.target.value']) + input(value: string) { + // + const res = this.getUnformattedValue(value); + this.onChange(res); + } + + @HostListener('blur') + onBlur() { + this.onTouched(); + } + + @HostListener('keydown', ['$event']) + onKeyDown(event: any) { + if (this.enabledCurrencyFormatter) { + this.setOnlyNumberType(event); + } + } + //#endregion + + // + //#region Implemenntation of Interface ... + writeValue(value: string): void { + this.setValue(value); + } + + registerOnChange(fn: (_: string | any | null) => void): void { + this.onChange = value => { + // + fn(this.enabledCurrencyFormatter ? +value : value); + this.setValue(value); + }; + } + + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.renderer.setProperty( + this.elementRef.nativeElement, + 'disabled', + isDisabled + ); + } + //#endregion + + // + //#region Private ... + private isElementValid() { + return this.elementRef && this.elementRef.nativeElement; + } + + private getFormattedValue(value: string) { + // + if (!this.enabledCurrencyFormatter || !value) { + return value || ''; + } + + // + if (this.applyLocaleToCurrency) { + value = applyLocale(value, this.locale || this.config.defaultLanguage); + } + + // + const result = value + .toString() + .replace(/\B(?=(\d{3})+(?!\d))/g, this.currencySeperator); + + // + return result; + } + + private getUnformattedValue(value: string) { + // + const result = removeAll(value, this.currencySeperator, true); + return result; + } + + private getNormalizedValue(value: string) { + // + if (!this.isElementValid()) { + return value; + } + + // + value = isNullOrEmptyString(value) ? '' : this.getUnformattedValue(value); + + // + if (this.enabledCurrencyFormatter) { + value = this.getFormattedValue(value); + } else { + value = this.getUnformattedValue(value); + } + + // + return value; + } + + private setValue(value: string) { + // + const normalizedValue = this.getNormalizedValue(value); + this.renderer.setProperty( + this.elementRef.nativeElement, + 'value', + normalizedValue + ); + } + + private setOnlyNumberType(e: any) { + // + const keyCode = e.code as string; + const isValidKey = + keyCode.includes('Digit') || keyCode.includes('Backspace'); + + // + if (!isValidKey) { + e.preventDefault(); + } + } + //#endregion +} diff --git a/projects/x-framework-services/src/lib/directives/x-date-presenter.directive.ts b/projects/x-framework-services/src/lib/directives/x-date-presenter.directive.ts new file mode 100644 index 0000000..4505f6c --- /dev/null +++ b/projects/x-framework-services/src/lib/directives/x-date-presenter.directive.ts @@ -0,0 +1,88 @@ +import { + XLocale, + formatDate, + getDateOfBirthDateFormatString, +} from 'x-framework-core'; +import { XLocalizationService } from '../x-localization.service'; +import { + Directive, + ElementRef, + OnInit, + Input, + OnChanges, + SimpleChanges, +} from '@angular/core'; + +@Directive({ + // tslint:disable-next-line:directive-selector + selector: '[xDatePresenter]', +}) +export class XDatePresenterDirective implements OnInit, OnChanges { + // + @Input() + date?: Date | string; + + @Input() + locale?: XLocale; + + @Input() + format?: string; + + @Input() + isUtc?: boolean; + + @Input() + toUtc?: boolean; + + constructor( + private element: ElementRef, + private localizationService: XLocalizationService + ) {} + + ngOnInit() { + this.presentDate(); + } + + ngOnChanges(changes: SimpleChanges): void { + this.presentDate(); + } + + private presentDate() { + // + if (!this.locale) { + this.locale = this.localizationService.locale; + } + + // + if (!this.date) { + this.date = new Date(); + } + + // + if (!this.format) { + this.format = getDateOfBirthDateFormatString(this.locale); + } + + // + if (!this.toUtc) { + this.toUtc = false; + } + + // + let dateStr: string; + try { + dateStr = formatDate( + this.date, + this.locale, + this.format, + this.isUtc, + this.toUtc + ); + } catch { + dateStr = '---'; + } + + // + this.element.nativeElement.innerHTML = dateStr; + } +} diff --git a/projects/x-framework-services/src/lib/directives/x-from-input-formatter.directive.ts b/projects/x-framework-services/src/lib/directives/x-from-input-formatter.directive.ts new file mode 100644 index 0000000..9eaf846 --- /dev/null +++ b/projects/x-framework-services/src/lib/directives/x-from-input-formatter.directive.ts @@ -0,0 +1,493 @@ +import { + Input, + Inject, + Directive, + Renderer2, + ElementRef, + forwardRef, + HostListener, +} from '@angular/core'; +import { + XLocale, + hasChild, + removeAll, + getDigits, + toXString, + applyLocale, + getNonDigits, + removeSpaces, + isNullOrUndefined, + extractUniqueChars, + isNullOrEmptyString, +} from 'x-framework-core'; +import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'; +import { X_FRAMEWORK_SERVICES_CONFIG } from '../tokens/x-injectable-tokens'; +import { XFrameworkServicesConfig } from '../config/x-framework-services.config'; + +export const X_FORM_INPUT_FORMATTER: any = { + useExisting: forwardRef(() => XFromInputFormatterDirective), + provide: NG_VALUE_ACCESSOR, + multi: true, +}; + +export enum XFormInputFormatter { + ApplyLocale = 'apply_locale', + FormatCurrency = 'format_currency', + StringFormatter = 'string_formatter', + StringInputFormatter = 'string_input_formatter', + FormatCurrencyAndApplyLocale = 'format_currency_apply_locale', + StringFormatterAndApplyLocale = 'string_formatter_apply_locale', + StringInputFormatterAndApplyLocale = 'string_input_formatter_apply_locale', +} + +export type XFormInputFormatterIdentifier = XFormInputFormatter | string; + +export enum XStringInputFormatType { + None = 'none', + LowerCase = 'lowercase', + UpperCase = 'uppercase', + NonSpace = 'non_space', + NonDigits = 'none_digits', + OnlyDigits = 'only_digits', + UniqueChars = 'unique_chars', +} + +export type XStringInputFormatTypeIdentifier = XStringInputFormatType | string; + +@Directive({ + // tslint:disable-next-line:directive-selector + selector: '[xFromInputFormatter]', + providers: [X_FORM_INPUT_FORMATTER], +}) +export class XFromInputFormatterDirective implements ControlValueAccessor { + // + //#region Props ... + @Input() + locale: XLocale; + + @Input() + inputFormatterType: XFormInputFormatterIdentifier; + + @Input() + stringInputFormatterType: XStringInputFormatTypeIdentifier; + + @Input() + seperator: string; + + @Input() + currencyLable: string; + + @Input() + stringFormatter: number[]; + + @Input() + enabledInputFormatter: boolean; + + // + onChange = (_: any) => {}; + + onTouched = () => {}; + + onFocused = () => {}; + //#endregion + + // + //#region Constructor ... + constructor( + private renderer: Renderer2, + private elementRef: ElementRef, + @Inject(X_FRAMEWORK_SERVICES_CONFIG) + private config: XFrameworkServicesConfig + ) {} + //#endregion + + // + //#region Type Detectors ... + isCurrencyType() { + return ( + this.inputFormatterType === XFormInputFormatter.FormatCurrency || + this.inputFormatterType === + XFormInputFormatter.FormatCurrencyAndApplyLocale + ); + } + + isStringFormatterType() { + return ( + this.inputFormatterType === XFormInputFormatter.StringFormatter || + this.inputFormatterType === + XFormInputFormatter.StringFormatterAndApplyLocale || + this.inputFormatterType === XFormInputFormatter.StringInputFormatter || + this.inputFormatterType === + XFormInputFormatter.StringInputFormatterAndApplyLocale + ); + } + + isLocaleType() { + return ( + this.inputFormatterType === XFormInputFormatter.ApplyLocale || + this.inputFormatterType === + XFormInputFormatter.FormatCurrencyAndApplyLocale || + this.inputFormatterType === + XFormInputFormatter.StringFormatterAndApplyLocale || + this.inputFormatterType === + XFormInputFormatter.StringInputFormatterAndApplyLocale + ); + } + //#endregion + + // + //#region Host Bindings ... + @HostListener('change', ['$event.target.value']) + change(value: string) { + // + const res = this.getUnformattedValue(value); + this.onChange(res); + } + + @HostListener('input', ['$event.target.value']) + input(value: string) { + // + const res = this.getUnformattedValue(value); + this.onChange(res); + } + + @HostListener('focus') + onFocus() { + this.onFocused(); + if (this.isCurrencyType()) { + this.clearCurrencyLabel(); + } + } + + @HostListener('blur') + onBlur() { + this.onTouched(); + if (this.isCurrencyType()) { + this.applyCurrencyLabel(); + } + } + + @HostListener('keydown', ['$event']) + onKeyDown(event: any) { + // if (this.isCurrencyType()) { + // this.setOnlyNumberType(event); + // } + } + //#endregion + + // + //#region Implemenntation of Interface ... + writeValue(value: string): void { + this.setValue(value); + } + + registerOnChange(fn: (_: string | any | null) => void): void { + this.onChange = (value) => { + // + fn(this.isCurrencyType() ? +value : value); + + // + this.setValue(value); + }; + } + + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + setDisabledState(isDisabled: boolean): void { + // + if (isDisabled && this.isCurrencyType()) { + this.applyCurrencyLabel(); + } + + // + this.renderer.setProperty( + this.elementRef.nativeElement, + 'disabled', + isDisabled + ); + } + //#endregion + + // + //#region Private ... + private isElementValid() { + return this.elementRef && this.elementRef.nativeElement; + } + + private getFormattedValue(value: string) { + // + // Normallize Value ... + if (isNullOrEmptyString(value) || !this.enabledInputFormatter) { + return ''; + } + + // + // Prepared Formatted Value ... + let result = this.isCurrencyType() + ? this.getCurrencyFormattedValue(value) + : this.isStringFormatterType() + ? this.getStringFormattedValue(value) + : value || ''; + + // + // Apply Locale if Localeable Input Type ... + if (this.isLocaleType()) { + result = this.getLocaleFormattedValue(result); + } + + // + return result; + } + + private getUnformattedValue(value: string) { + // + let result = value.toString() || ''; + + // + const seperator = isNullOrEmptyString(this.seperator) ? '' : this.seperator; + + // + // Remove All occurence of seperator ... + if (this.enabledInputFormatter) { + result = removeAll(value, seperator, true); + } + + // + // Currency Lable ... + if (this.isCurrencyType() && !isNullOrEmptyString(this.currencyLable)) { + result = result.replace(this.currencyLable, '').trim(); + } + + // + // Change Locale to Default ... + if (this.isLocaleType()) { + result = !isNullOrEmptyString(result) ? applyLocale(result, 'en-US') : ''; + } + + // + return result; + } + + private getNormalizedValue(value: string) { + // + if (!this.isElementValid()) { + return value; + } + + // + // Normallize Value and prevent from null ... + let result = value ? value.toString() || '' : ''; + result = isNullOrEmptyString(value) ? '' : this.getUnformattedValue(value); + if (!this.enabledInputFormatter) { + return result; + } + + // + // Handle Apply Locale ... + if (this.inputFormatterType === XFormInputFormatter.ApplyLocale) { + // + result = applyLocale(value, this.locale || this.config.defaultLanguage); + return result; + } + + // + // Handle Formatter ... + if (this.isCurrencyType() || this.isStringFormatterType()) { + return this.getFormattedValue(result); + } + + // + return value; + } + + private setValue(value: string, normalizeValue = true) { + // + const normalizedValue = normalizeValue + ? this.getNormalizedValue(value) + : value || ''; + this.renderer.setProperty( + this.elementRef.nativeElement, + 'value', + normalizedValue + ); + } + + private getValue() { + return this.elementRef.nativeElement.value + ? (this.elementRef.nativeElement.value as string) + : ''; + } + + private setOnlyNumberType(e: any) { + // + const keyCode = e.code as string; + const numPadKeys = [...new Array(10).keys()].map((c, i) => `Numpad${i}`); + const isValidKey = + keyCode.includes('Tab') || + keyCode.includes('Digit') || + keyCode.includes('Backspace') || + numPadKeys.includes(keyCode); + + // + if (!isValidKey) { + e.preventDefault(); + } + } + + // + //#region Formatter Actions ... + private getLocaleFormattedValue(value: any) { + // + const mValue = value ? (value.toString() as string) : ''; + return applyLocale(mValue, this.locale || this.config.defaultLanguage); + } + + private getStringFormattedValue(value: any) { + // + const mValue = !isNullOrUndefined(value) ? toXString(value) : ''; + + // + let mResultValue = ''; + + // + if ( + this.inputFormatterType === XFormInputFormatter.StringFormatter || + this.inputFormatterType === + XFormInputFormatter.StringFormatterAndApplyLocale + ) { + // + if (!hasChild(this.stringFormatter)) { + return mValue; + } + + // + let mSource = mValue; + this.stringFormatter.forEach((l, i) => { + if (mSource.length >= l) { + // + const selected = mSource.substring(0, l); + + // + mResultValue += + i === this.stringFormatter.length + ? selected + : `${selected}${this.seperator}`; + + // + mSource = mSource.substr(l, mValue.length); + } + }); + + // + if (mSource.length > 0) { + mResultValue += `${mSource}`; + } + } else if ( + this.inputFormatterType === XFormInputFormatter.StringInputFormatter || + this.inputFormatterType === + XFormInputFormatter.StringInputFormatterAndApplyLocale + ) { + // + switch (this.stringInputFormatterType) { + // + case XStringInputFormatType.LowerCase: + mResultValue = mValue.toLowerCase(); + break; + + // + case XStringInputFormatType.UpperCase: + mResultValue = mValue.toUpperCase(); + break; + + // + case XStringInputFormatType.NonDigits: + // + mResultValue = getNonDigits(mValue); + break; + + // + case XStringInputFormatType.UniqueChars: + // + mResultValue = extractUniqueChars(mValue); + break; + + // + case XStringInputFormatType.NonSpace: + mResultValue = removeSpaces(mValue.trim()); + break; + + // + case XStringInputFormatType.OnlyDigits: + mResultValue = getDigits(mValue); + break; + + // + default: + mResultValue = mValue; + break; + } + } + + // + return mResultValue; + } + + private getCurrencyFormattedValue(value: any) { + // + const mValue = value ? getDigits(value.toString() as string) : ''; + const seperator = isNullOrEmptyString(this.seperator) ? '' : this.seperator; + const formattedValue = mValue.replace(/\B(?=(\d{3})+(?!\d))/g, seperator); + + // + const result = !isNullOrEmptyString(formattedValue) ? formattedValue : ''; + + // + return result; + } + + private applyCurrencyLabel() { + // + if (!this.isCurrencyType()) { + return; + } + + // + let value = this.getValue(); + + // + if ( + !isNullOrEmptyString(value) && + !isNullOrEmptyString(this.currencyLable) + ) { + value = `${value} ${this.currencyLable}`; + } + + // + this.setValue(value, false); + } + + private clearCurrencyLabel() { + // + if (!this.isCurrencyType()) { + return; + } + + // + let value = this.getValue(); + + // + if ( + !isNullOrEmptyString(value) && + !isNullOrEmptyString(this.currencyLable) + ) { + value = value.replace(this.currencyLable, ''); + } + + // + this.setValue(value); + } + //#endregion + //#endregion +} diff --git a/projects/x-framework-services/src/lib/directives/x-locale-input-formatter.directive.ts b/projects/x-framework-services/src/lib/directives/x-locale-input-formatter.directive.ts new file mode 100644 index 0000000..1f63fb9 --- /dev/null +++ b/projects/x-framework-services/src/lib/directives/x-locale-input-formatter.directive.ts @@ -0,0 +1,137 @@ +import { + Input, + Inject, + Directive, + Renderer2, + ElementRef, + forwardRef, + HostListener, +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { X_FRAMEWORK_SERVICES_CONFIG } from '../tokens/x-injectable-tokens'; +import { XLocale, applyLocale, isNullOrEmptyString } from 'x-framework-core'; +import { XFrameworkServicesConfig } from '../config/x-framework-services.config'; + +export const X_LOCALE_FORMATTER: any = { + useExisting: forwardRef(() => XLocaleInputFormatterDirective), + provide: NG_VALUE_ACCESSOR, + multi: true, +}; + +@Directive({ + // tslint:disable-next-line:directive-selector + selector: '[xLocaleInputFormatter]', + providers: [X_LOCALE_FORMATTER], +}) +export class XLocaleInputFormatterDirective implements ControlValueAccessor { + // + //#region Props ... + @Input() + locale: XLocale; + + @Input() + enabledLocaleFormatter: boolean; + + onChange = (_: any) => {}; + + onTouched = () => {}; + //#endregion + + // + //#region Constructor ... + constructor( + private renderer: Renderer2, + private elementRef: ElementRef, + @Inject(X_FRAMEWORK_SERVICES_CONFIG) + private config: XFrameworkServicesConfig + ) {} + //#endregion + + // + //#region Host Bindings ... + @HostListener('change', ['$event.target.value']) + change(value: string) { + this.onChange(value); + } + + @HostListener('input', ['$event.target.value']) + input(value: string) { + this.onChange(value); + } + + @HostListener('blur') + onBlur() { + this.onTouched(); + } + //#endregion + + // + //#region Implemenntation of Interface ... + writeValue(value: string): void { + this.setValue(value); + } + + registerOnChange(fn: (_: string | any | null) => void): void { + this.onChange = (value) => { + // + fn(value); + this.setValue(value); + }; + } + + registerOnTouched(fn: () => void): void { + this.onTouched = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.renderer.setProperty( + this.elementRef.nativeElement, + 'disabled', + isDisabled + ); + } + //#endregion + + // + //#region Private ... + private isElementValid() { + return this.elementRef && this.elementRef.nativeElement; + } + + private getFormattedValue(value: string) { + // + if (!this.enabledLocaleFormatter || !value) { + return value || ''; + } + + // + value = applyLocale(value, this.locale || this.config.defaultLanguage); + + // + return value; + } + + private getNormalizedValue(value: string) { + // + if (!this.isElementValid()) { + return value; + } + + // + value = isNullOrEmptyString(value) ? '' : this.getFormattedValue(value); + + // + return value; + } + + private setValue(value: string) { + // + const normalizedValue = this.getNormalizedValue(value); + this.renderer.setProperty( + this.elementRef.nativeElement, + 'value', + normalizedValue + ); + } + //#endregion +} diff --git a/projects/x-framework-services/src/lib/directives/x-swipable.directive.ts b/projects/x-framework-services/src/lib/directives/x-swipable.directive.ts new file mode 100644 index 0000000..104da16 --- /dev/null +++ b/projects/x-framework-services/src/lib/directives/x-swipable.directive.ts @@ -0,0 +1,326 @@ +import { + Input, + OnInit, + Output, + OnChanges, + Directive, + OnDestroy, + ElementRef, + EventEmitter, + SimpleChanges, + AfterViewInit, +} from '@angular/core'; +import { Subject, Observable } from 'rxjs'; +import { keys, nameof } from 'x-framework-core'; +import { GestureController } from '@ionic/angular'; +import { Gesture, GestureDetail } from '@ionic/core'; +import { takeUntil, debounceTime } from 'rxjs/operators'; +import { XSettingsService } from '../x-settings.service'; + +export enum XSwipeDirection { + UP = 'up', + DOWN = 'down', + LEFT = 'left', + START = 'start', + RIGHT = 'right', + END = 'end', +} + +export type XSwipeDirectionIdentifier = XSwipeDirection | string; + +export enum XSwipeSupportedDirections { + VERTICALL, + HORIZONTAL_LEFT_RIGHT, + HORIZONTAL_START_END, + ALL, +} + +export interface XSwipeEvent { + deltaX: number; + deltaY: number; + direction: XSwipeDirectionIdentifier; +} + +@Directive({ + // tslint:disable-next-line:directive-selector + selector: '[xSwipable]', +}) +export class XSwipableDirective + implements OnInit, AfterViewInit, OnDestroy, OnChanges { + // + //#region Props ... + private gesture: Gesture; + private unsubscriber = new Subject(); + private issueSwipeEvents$ = new Subject(); + private uiDirection = this.settingsService.uiDirection; + //#endregion + + // + //#region Inputs ... + @Input() + threshold = 60; + + @Input() + swipeDisabled = false; + + @Input() + debounceTime = 300; + + @Input() + supportedDirections: XSwipeSupportedDirections = + XSwipeSupportedDirections.HORIZONTAL_START_END; + //#endregion + + // + //#region Event Emitters ... + @Output() + swipe = new EventEmitter(); + + @Output() + swipeUp = new EventEmitter(); + + @Output() + swipeDown = new EventEmitter(); + + @Output() + swipeLeft = new EventEmitter(); + + @Output() + swipeStart = new EventEmitter(); + + @Output() + swipeRight = new EventEmitter(); + + @Output() + swipeEnd = new EventEmitter(); + //#endregion + + // + //#region Constructor ... + constructor( + private elementRef: ElementRef, + private gestureCtrl: GestureController, + private settingsService: XSettingsService + ) {} + //#endregion + + // + //#region LifeCycles ... + ngOnInit() { + this.prepareGesture(); + } + + ngAfterViewInit() { + this.registerIssueSwipeHandler(); + } + + ngOnDestroy() { + this.destroyGesture(); + this.issueSwipeEvents$.complete(); + this.releaseUnsubscriber(); + } + + ngOnChanges(changes: SimpleChanges) { + // + const changeKesy = keys(changes); + + // + const isThresholdChanged = changeKesy.includes( + nameof('threshold') + ); + if (isThresholdChanged) { + this.prepareGesture(); + } + + // + const isDebounceTimeChanged = changeKesy.includes( + nameof('debounceTime') + ); + if (isDebounceTimeChanged) { + this.registerIssueSwipeHandler(); + } + } + //#endregion + + // + //#region Private ... + private prepareGesture() { + // + this.destroyGesture(); + + // + this.gesture = this.gestureCtrl.create({ + el: this.elementRef.nativeElement, + threshold: this.threshold, + gestureName: 'x-gesture', + onMove: (detail) => { + this.onMoveHandler(detail); + }, + }); + + // + this.gesture.enable(); + } + + private destroyGesture() { + // + if (this.gesture) { + // + this.gesture.destroy(); + this.gesture = null; + } + } + + private releaseUnsubscriber() { + // + if (!this.unsubscriber.closed) { + this.unsubscriber.next(); + this.unsubscriber.complete(); + } + } + + private applyTakeUntilWrapper(task: Observable): Observable { + return task.pipe(takeUntil(this.unsubscriber)); + } + + private registerIssueSwipeHandler() { + // + this.applyTakeUntilWrapper( + this.issueSwipeEvents$ + .asObservable() + .pipe(debounceTime(this.debounceTime)) + ).subscribe((gDetail) => { + // + if (this.swipeDisabled) { + return; + } + + // + const mSwipeEvent: Partial = { + deltaX: gDetail.deltaX, + deltaY: gDetail.deltaY, + }; + + // + // Handle Vertical ... + if ( + this.supportedDirections === XSwipeSupportedDirections.ALL || + this.supportedDirections === XSwipeSupportedDirections.VERTICALL + ) { + // + let swipeDir: XSwipeDirection; + + // + if (gDetail.deltaY >= 0) { + // + swipeDir = XSwipeDirection.DOWN; + + // + this.swipeDown.emit(); + } else { + // + swipeDir = XSwipeDirection.UP; + + // + this.swipeUp.emit(); + } + + // + // Handle Global Event ... + mSwipeEvent.direction = swipeDir; + this.swipe.emit(mSwipeEvent as XSwipeEvent); + } + + // + // Handle Horizontal Left Right ... + if ( + this.supportedDirections === XSwipeSupportedDirections.ALL || + this.supportedDirections === + XSwipeSupportedDirections.HORIZONTAL_LEFT_RIGHT + ) { + // + let swipeDir: XSwipeDirection; + + // + if (gDetail.deltaX >= 0) { + // + swipeDir = XSwipeDirection.RIGHT; + + // + this.swipeRight.emit(); + } else { + // + swipeDir = XSwipeDirection.LEFT; + + // + this.swipeLeft.emit(); + } + + // + // Handle Global Event ... + mSwipeEvent.direction = swipeDir; + this.swipe.emit(mSwipeEvent as XSwipeEvent); + } + + // + // Handle Horizontal Start End ... + if ( + this.supportedDirections === XSwipeSupportedDirections.ALL || + this.supportedDirections === + XSwipeSupportedDirections.HORIZONTAL_START_END + ) { + // + let swipeDir: XSwipeDirection; + + // + if (gDetail.deltaX >= 0) { + // + if (this.uiDirection === 'ltr') { + // + swipeDir = XSwipeDirection.END; + + // + this.swipeEnd.emit(); + } else { + // + swipeDir = XSwipeDirection.START; + + // + this.swipeStart.emit(); + } + } else { + // + if (this.uiDirection === 'ltr') { + // + swipeDir = XSwipeDirection.START; + + // + this.swipeStart.emit(); + } else { + // + swipeDir = XSwipeDirection.END; + + // + this.swipeEnd.emit(); + } + } + + // + // Handle Global Event ... + mSwipeEvent.direction = swipeDir; + this.swipe.emit(mSwipeEvent as XSwipeEvent); + } + }); + } + + private onMoveHandler(ev: GestureDetail) { + // + if (this.swipeDisabled) { + return; + } + + // + this.issueSwipeEvents$.next(ev); + } + //#endregion +} diff --git a/projects/x-framework-services/src/lib/modules/x-directives.module.ts b/projects/x-framework-services/src/lib/modules/x-directives.module.ts new file mode 100644 index 0000000..a405596 --- /dev/null +++ b/projects/x-framework-services/src/lib/modules/x-directives.module.ts @@ -0,0 +1,44 @@ +import { NgModule } from '@angular/core'; +import { IonicModule } from '@ionic/angular'; +import { CommonModule } from '@angular/common'; +import { XTranslatePipe } from '../pipes/x-translate.pipe'; +import { XSwipableDirective } from '../directives/x-swipable.directive'; +import { XDatePresenterDirective } from '../directives/x-date-presenter.directive'; +import { XCurrencyFormatterDirective } from '../directives/x-currency-formatter.directive'; +import { XFromInputFormatterDirective } from '../directives/x-from-input-formatter.directive'; +import { XLocaleInputFormatterDirective } from '../directives/x-locale-input-formatter.directive'; + +@NgModule({ + declarations: [ + // + // Pipes ... + XTranslatePipe, + + // + // Directives + XSwipableDirective, + XDatePresenterDirective, + XCurrencyFormatterDirective, + XFromInputFormatterDirective, + XLocaleInputFormatterDirective + ], + imports: [CommonModule, IonicModule], + exports: [ + // + CommonModule, + IonicModule, + + // + // Pipes ... + XTranslatePipe, + + // + // Directives + XSwipableDirective, + XDatePresenterDirective, + XCurrencyFormatterDirective, + XFromInputFormatterDirective, + XLocaleInputFormatterDirective + ] +}) +export class XDirectivesModule {} diff --git a/projects/x-framework-services/src/lib/pipes/x-translate.pipe.ts b/projects/x-framework-services/src/lib/pipes/x-translate.pipe.ts new file mode 100644 index 0000000..0aa5753 --- /dev/null +++ b/projects/x-framework-services/src/lib/pipes/x-translate.pipe.ts @@ -0,0 +1,55 @@ +import { + XLocale, + toNumber, + isString, + XResourceIDs, + XTranslationItem +} from 'x-framework-core'; +import { Pipe, PipeTransform } from '@angular/core'; +import { XLocalizationService } from '../x-localization.service'; + +@Pipe({ + name: 'xTranslate' +}) +export class XTranslatePipe implements PipeTransform { + // + constructor(private localizationService: XLocalizationService) {} + + /** + * translate a resource to specific language in a pipe + * + * @param key the resource identifier + * @param locale the selected or default locale + */ + transform( + key: XResourceIDs | XTranslationItem | string, + locale?: XLocale + ): string { + // + if (!locale) { + locale = this.localizationService.locale; + } + + // + const isStringKey = isString(key); + if (!isStringKey) { + key = toNumber(key) as XResourceIDs; + } + + // + const isExist = isStringKey + ? this.localizationService.isExistsIdOfLocale(locale, key as string) + : this.localizationService.isExistsIdOfLocale( + locale, + key as XResourceIDs + ); + if (!isExist) { + return ''; + } + + // + return isStringKey + ? this.localizationService.getValueOfLocale(locale, key as string) + : this.localizationService.getValueOfLocale(locale, key as XResourceIDs); + } +} diff --git a/projects/x-framework-services/src/lib/tokens/x-injectable-tokens.ts b/projects/x-framework-services/src/lib/tokens/x-injectable-tokens.ts new file mode 100644 index 0000000..af36ea5 --- /dev/null +++ b/projects/x-framework-services/src/lib/tokens/x-injectable-tokens.ts @@ -0,0 +1,6 @@ +import { InjectionToken } from '@angular/core'; +import { XFrameworkServicesConfig } from '../config/x-framework-services.config'; + +export const X_FRAMEWORK_SERVICES_CONFIG = new InjectionToken< + XFrameworkServicesConfig +>('x_config'); diff --git a/projects/x-framework-services/src/lib/x-dialog.service.ts b/projects/x-framework-services/src/lib/x-dialog.service.ts new file mode 100644 index 0000000..56e3353 --- /dev/null +++ b/projects/x-framework-services/src/lib/x-dialog.service.ts @@ -0,0 +1,576 @@ +import { + NavController, + AlertController, + ModalController, + PickerController, + PopoverController, + LoadingController, + ActionSheetController, +} from '@ionic/angular'; +import { + toArray, + hasChild, + XActionType, + XModalInput, + XBaseService, + XResourceIDs, + XModalButton, + XPickerColumn, + XAlertOptions, + XModalOptions, + XPickerOptions, + XPopoverOptions, + XLoadingOptions, + XModalButtonRole, + XActionModalOptions, + XPickerColumnOption, + XModalButtonRoleIdentifier, +} from 'x-framework-core'; +import { Observable, of } from 'rxjs'; +import { Injectable, Inject } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { XLocalizationService } from './x-localization.service'; +import { XNotificationService } from './x-notification.service'; +import { X_FRAMEWORK_SERVICES_CONFIG } from './tokens/x-injectable-tokens'; +import { XFrameworkServicesConfig } from './config/x-framework-services.config'; + +@Injectable({ + providedIn: 'root', +}) +export class XDialogService extends XBaseService { + // + constructor( + @Inject(X_FRAMEWORK_SERVICES_CONFIG) config: XFrameworkServicesConfig, + public matDialogService: MatDialog, + public modalController: ModalController, + public alertController: AlertController, + public pickerController: PickerController, + public navigationController: NavController, + public popoverController: PopoverController, + public loadingController: LoadingController, + private notificationService: XNotificationService, + private localizationService: XLocalizationService, + public actionSheetController: ActionSheetController + ) { + super(config); + } + + // + //#region Service Actions ... + nativeAlert(message?: string | XResourceIDs): Observable { + // + message = !message ? XResourceIDs.are_you_sure : message; + + // + const mMessage = this.localizationService.getValue(message); + const confirmation = window.alert(mMessage); + + // + return of(confirmation); + } + + nativeConfirm(message?: string | XResourceIDs): Observable { + // + message = !message ? XResourceIDs.are_you_sure : message; + + // + const mMessage = this.localizationService.getValue(message); + const confirmation = window.confirm(mMessage); + + // + return of(confirmation); + } + + async presentActionModal(props: XActionModalOptions) { + // + const options = await this.toActionSheetOptions(props); + const actionModal = await this.actionSheetController.create(options); + + // + if (props.playSound) { + this.notificationService.playSound(props.type || XActionType.System); + } + + // + await actionModal.present(); + return actionModal; + } + + async presentModal(props: XModalOptions) { + // + if (props.fullScreen) { + // + const mFullscreen = 'fullscreen'; + if (hasChild(props.cssClass)) { + props.cssClass = [...toArray(props.cssClass), mFullscreen]; + } else { + props.cssClass = [mFullscreen]; + } + } else if (props.autoHeight) { + // + const mAutoHeight = 'autoHeight'; + if (hasChild(props.cssClass)) { + props.cssClass = [...toArray(props.cssClass), mAutoHeight]; + } else { + props.cssClass = [mAutoHeight]; + } + } + + // + const modal = await this.modalController.create(props); + await modal.present(); + return modal; + } + + async presentPopover(props: XPopoverOptions) { + // + const popover = await this.popoverController.create(props); + await popover.present(); + return popover; + } + + async presentLoading(props: XLoadingOptions) { + // + const loading = await this.loadingController.create(props); + await loading.present(); + return loading; + } + + async presentAlert(props: XAlertOptions) { + // + const options = await this.prepareAlertOptions(props); + const alert = await this.alertController.create(options); + + // + await alert.present(); + return alert; + } + + async presentPicker(props: XPickerOptions) { + // + const options = await this.preparePickerOptions(props); + const picker = await this.pickerController.create(options); + + // + await picker.present(); + return picker; + } + //#endregion + + // + //#region Commonly Used dialogs ... + async presentAreYouSureOkCancleDialog( + okHandler: () => void, + okRole?: XModalButtonRoleIdentifier, + cancelHandler?: () => void, + cancelRole?: XModalButtonRoleIdentifier + ) { + await this.presentActionModal({ + header: XResourceIDs.are_you_sure, + buttons: [], + keyboardClose: true, + backdropDismiss: true, + hasOk: true, + okRole, + okHandler, + hasCancel: true, + cancelRole, + cancelHandler, + }); + } + + async presentAreYouSureYesNoDialog( + yesHandler: () => void, + yesRole?: XModalButtonRoleIdentifier, + noHandler?: () => void, + noRole?: XModalButtonRoleIdentifier + ) { + await this.presentActionModal({ + header: XResourceIDs.are_you_sure, + buttons: [], + keyboardClose: true, + backdropDismiss: true, + hasNo: true, + noRole, + noHandler, + hasYes: true, + yesRole, + yesHandler, + }); + } + //#endregion + + // + //#region Private ... + // + //#region Button Preparer ... + getButton( + text?: XResourceIDs | string, + icon?: string, + role?: XModalButtonRoleIdentifier, + handler?: () => boolean | void | Promise + ) { + // + if (!role) { + role = XModalButtonRole.Selected; + } + + // + const mText = this.getPreparedMessage(text); + return { + text: mText, + icon, + role, + handler, + }; + } + + getYesButton( + role?: XModalButtonRoleIdentifier, + handler?: () => boolean | void | Promise + ) { + // + if (!role) { + role = XModalButtonRole.Selected; + } + + // + const mText = this.getPreparedMessage(XResourceIDs.yes); + const mIcon = 'checkmark'; + + return { + text: mText, + icon: mIcon, + role, + handler, + }; + } + + getNoButton( + role?: XModalButtonRoleIdentifier, + handler?: () => boolean | void | Promise + ) { + // + if (!role) { + role = XModalButtonRole.Selected; + } + + // + const mText = this.getPreparedMessage(XResourceIDs.no); + const mIcon = 'close'; + + return { + text: mText, + icon: mIcon, + role, + handler, + }; + } + + getOkButton( + role?: XModalButtonRoleIdentifier, + handler?: () => boolean | void | Promise + ) { + // + if (!role) { + role = XModalButtonRole.Selected; + } + + // + const mText = this.getPreparedMessage(XResourceIDs.ok); + const mIcon = 'checkmark'; + + return { + text: mText, + icon: mIcon, + role, + handler, + }; + } + + getCancelButton( + role?: XModalButtonRoleIdentifier, + handler?: () => boolean | void | Promise + ) { + // + if (!role) { + role = XModalButtonRole.Selected; + } + + // + const mText = this.getPreparedMessage(XResourceIDs.cancel); + const mIcon = 'close'; + + return { + text: mText, + icon: mIcon, + role, + handler, + }; + } + + getAcceptButton( + role?: XModalButtonRoleIdentifier, + handler?: () => boolean | void | Promise + ) { + // + if (!role) { + role = XModalButtonRole.Selected; + } + + // + const mText = this.getPreparedMessage(XResourceIDs.accept); + const mIcon = 'checkmark'; + + return { + text: mText, + icon: mIcon, + role, + handler, + }; + } + + getRetryButton( + role?: XModalButtonRoleIdentifier, + handler?: () => boolean | void | Promise + ) { + // + if (!role) { + role = XModalButtonRole.Selected; + } + + // + const mText = this.getPreparedMessage(XResourceIDs.retry); + const mIcon = 'refresh'; + + return { + text: mText, + icon: mIcon, + role, + handler, + }; + } + + getDismissButton( + role?: XModalButtonRoleIdentifier, + handler?: () => boolean | void | Promise + ) { + // + if (!role) { + role = XModalButtonRole.Destructive; + } + + // + const mText = this.getPreparedMessage(XResourceIDs.dismiss); + const mIcon = 'remove-circle-outline'; + + return { + text: mText, + icon: mIcon, + role, + handler, + }; + } + //#endregion + + private prepareModalButton(button?: XModalButton) { + // + const alertButton = { + ...button, + text: button.text ? this.getPreparedMessage(button.text) : '', + }; + + // + return alertButton; + } + + private async prepareModalButtons(buttons?: XModalButton[]) { + // + if (!hasChild(buttons)) { + return []; + } + + // + return buttons.map((b) => this.prepareModalButton(b)); + } + + private prepareModalInput(input?: XModalInput) { + // + const alertInput = { + ...input, + label: input.label ? this.getPreparedMessage(input.label) : '', + placeholder: input.placeholder + ? this.getPreparedMessage(input.placeholder) + : '', + }; + + // + return alertInput; + } + + private async prepareModalInputs(inputs?: XModalInput[]) { + // + if (!hasChild(inputs)) { + return []; + } + + // + return inputs.map((i) => this.prepareModalInput(i)); + } + + private async prepareAlertOptions(options?: XAlertOptions) { + // + const alertOptions = { + ...options, + header: options.header ? this.getPreparedMessage(options.header) : '', + subHeader: options.subHeader + ? this.getPreparedMessage(options.subHeader) + : '', + message: options.message ? this.getPreparedMessage(options.message) : '', + inputs: hasChild(options.inputs) + ? await this.prepareModalInputs(options.inputs) + : [], + buttons: hasChild(options.buttons) + ? await this.prepareModalButtons(options.buttons) + : [], + }; + + // + return alertOptions; + } + + private preparePickerColumnOption(columnOption?: XPickerColumnOption) { + // + const pickerColumnOption = { + ...columnOption, + text: this.getPreparedMessage(columnOption.text), + }; + + // + return pickerColumnOption; + } + + private async preparePickerColumnOptions( + columnOptions?: XPickerColumnOption[] + ) { + // + if (!hasChild(columnOptions)) { + return []; + } + + // + return columnOptions.map((co) => this.preparePickerColumnOption(co)); + } + + private async preparePickerColumn(column?: XPickerColumn) { + // + const pickerColumn = { + ...column, + options: await this.preparePickerColumnOptions(column.options), + }; + + // + return pickerColumn; + } + + private async preparePickerColumns(columns: XPickerColumn[]) { + // + if (!hasChild(columns)) { + return []; + } + + // + return columns.map((c) => this.preparePickerColumn(c)); + } + + private async preparePickerOptions(options?: XPickerOptions) { + // + const pickerOptions = { + ...options, + buttons: (await this.prepareModalButtons(options.buttons)) || [], + columns: + (await Promise.all(await this.preparePickerColumns(options.columns))) || + [], + }; + + // + return pickerOptions; + } + + private async toActionSheetOptions(options?: XActionModalOptions) { + // + let buttons: any[] = []; + + // + const optButtons = (await this.prepareModalButtons(options.buttons)) || []; + buttons = [...buttons, ...optButtons]; + + // + //#region Handle Preserved Buttons ... + if (options.hasRetry) { + buttons.push( + this.getRetryButton(options.retryRole, options.retryHandler) + ); + } + + // + if (options.hasAccept) { + buttons.push( + this.getAcceptButton(options.acceptRole, options.acceptHandler) + ); + } + + // + if (options.hasOk) { + buttons.push(this.getOkButton(options.okRole, options.okHandler)); + } + + // + if (options.hasCancel) { + buttons.push( + this.getCancelButton(options.cancelRole, options.cancelHandler) + ); + } + + // + if (options.hasYes) { + buttons.push(this.getYesButton(options.yesRole, options.yesHandler)); + } + + // + if (options.hasNo) { + buttons.push(this.getNoButton(options.noRole, options.noHandler)); + } + + // + if (options.hasDismiss) { + buttons.push( + this.getDismissButton(options.dismissRole, options.dismissHandler) + ); + } + //#endregion + + // + const actionModalOptions = { + ...options, + header: options.header ? this.getPreparedMessage(options.header) : '', + subHeader: options.subHeader + ? this.getPreparedMessage(options.subHeader) + : '', + backdropDismiss: options.backdropDismiss || true, + buttons, + }; + + // + return actionModalOptions; + } + + private getPreparedMessage(key: string | number) { + return this.localizationService.isExistsId(key) + ? this.localizationService.getValue(key) + : key + ? key.toString() + : ''; + } + //#endregion +} diff --git a/projects/x-framework-services/src/lib/x-framework-services.module.ts b/projects/x-framework-services/src/lib/x-framework-services.module.ts new file mode 100644 index 0000000..0574aec --- /dev/null +++ b/projects/x-framework-services/src/lib/x-framework-services.module.ts @@ -0,0 +1,50 @@ +import { NgModule } from '@angular/core'; +import { IonicModule } from '@ionic/angular'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; +import { XDialogService } from './x-dialog.service'; +import { XHistoryService } from './x-history.service'; +import { XManagerService } from './x-manager.service'; +import { XStorageService } from './x-storage.service'; +import { HttpClientModule } from '@angular/common/http'; +import { XSecurityService } from './x-security.service'; +import { XSettingsService } from './x-settings.service'; +import { XGeolocationService } from './x-geolocation.service'; +import { XTaskManagerService } from './x-task-manager.service'; +import { XLocalizationService } from './x-localization.service'; +import { XNotificationService } from './x-notification.service'; +import { XThemeManagerService } from './x-theme-manager.service'; +import { XDirectivesModule } from './modules/x-directives.module'; +import { XSecureStorageService } from './x-secure-storage.service'; + +@NgModule({ + declarations: [], + imports: [ + IonicModule, + CommonModule, + RouterModule, + HttpClientModule, + XDirectivesModule, + ], + exports: [ + CommonModule, + HttpClientModule, + XDirectivesModule, + ], + providers: [ + // + XDialogService, + XStorageService, + XManagerService, + XHistoryService, + XSettingsService, + XSecurityService, + XGeolocationService, + XTaskManagerService, + XLocalizationService, + XNotificationService, + XThemeManagerService, + XSecureStorageService, + ] +}) +export class XFrameworkServicesModule {} diff --git a/projects/x-framework-services/src/lib/x-geolocation.service.ts b/projects/x-framework-services/src/lib/x-geolocation.service.ts new file mode 100644 index 0000000..acba004 --- /dev/null +++ b/projects/x-framework-services/src/lib/x-geolocation.service.ts @@ -0,0 +1,193 @@ +import { + getError, + Exceptions, + XException, + XBaseService, +} from 'x-framework-core'; +import { Observable } from 'rxjs'; +import { Injectable, Output, EventEmitter, Inject } from '@angular/core'; +import { X_FRAMEWORK_SERVICES_CONFIG } from './tokens/x-injectable-tokens'; +import { XFrameworkServicesConfig } from './config/x-framework-services.config'; + +@Injectable({ + providedIn: 'root', +}) +export class XGeolocationService extends XBaseService { + // + private IS_SUPPORT = false; + private NAVIGATION_WATCHER: any; + + // + options: PositionOptions; + + // + @Output() + changeLoading: EventEmitter = new EventEmitter(); + + // + isLoading = false; + + // + constructor( + @Inject(X_FRAMEWORK_SERVICES_CONFIG) config: XFrameworkServicesConfig + ) { + super(config); + + // + // Check Service is Supported or not ... + if (navigator.geolocation) { + this.IS_SUPPORT = true; + } + + // + this.options = { + ...config.geoLocationConfig, + }; + } + + /** + * determines service is Supported or not + */ + get isSupport() { + return this.IS_SUPPORT; + } + + /** + * get given position link for open on Google Maps ... + */ + getGoogleMapLink(position: GeolocationPosition) { + return `https://maps.google.com/?q=${position.coords.latitude},${position.coords.longitude}`; + } + + /** + * get given position Lat,Long string ... + */ + getLatLong(position: GeolocationPosition) { + return `${position.coords.latitude},${position.coords.longitude}`; + } + + /** + * get current location ... + */ + getCurrentLocation(): Observable { + // + this.startLoading(); + + return new Observable((observer) => { + // + if (!this.IS_SUPPORT) { + // + this.stopLoading(); + observer.error(getError(Exceptions.UnsupportedBrowser)); + } + + // + navigator.geolocation.getCurrentPosition( + (position) => { + // + this.stopLoading(); + + // + observer.next(position); + observer.complete(); + }, + (error) => { + // + this.stopLoading(); + + // + let exception: XException; + switch (error.code) { + case 1: + exception = Exceptions.PermissionDenied; + break; + + case 2: + exception = Exceptions.PositionUnavailable; + break; + + case 3: + default: + exception = Exceptions.Timeout; + break; + } + + // + observer.error(getError(exception)); + }, + { + ...this.options, + } + ); + }); + } + + /** + * start Watching Location Changes ... + */ + watchLocation(): Observable { + // + this.startLoading(); + + // + return new Observable((observer) => { + this.NAVIGATION_WATCHER = navigator.geolocation.watchPosition( + (position) => { + // + if (!this.IS_SUPPORT) { + // + this.stopLoading(); + observer.error(getError(Exceptions.UnsupportedBrowser)); + observer.complete(); + } + + // + observer.next(position); + }, + (error) => { + // + this.stopLoading(); + let exception: XException; + switch (error.code) { + case 1: + exception = Exceptions.PermissionDenied; + break; + + case 2: + exception = Exceptions.PositionUnavailable; + break; + + case 3: + default: + exception = Exceptions.Timeout; + break; + } + + // + observer.error(getError(exception)); + } + ); + }); + } + + /** + * Stop Watching Location Changes ... + */ + stopWatchingLocation() { + // + this.stopLoading(); + navigator.geolocation.clearWatch(this.NAVIGATION_WATCHER); + } + + private startLoading() { + // + this.isLoading = true; + this.changeLoading.emit(true); + } + + private stopLoading() { + // + this.isLoading = false; + this.changeLoading.emit(false); + } +} diff --git a/projects/x-framework-services/src/lib/x-history.service.ts b/projects/x-framework-services/src/lib/x-history.service.ts new file mode 100644 index 0000000..fa24c3f --- /dev/null +++ b/projects/x-framework-services/src/lib/x-history.service.ts @@ -0,0 +1,99 @@ +import { Subject } from 'rxjs'; +import { Injectable } from '@angular/core'; +import { Location, PlatformLocation } from '@angular/common'; + +@Injectable({ + providedIn: 'root', +}) +export class XHistoryService { + // + //#region Props ... + // + public readonly backHandler = new Subject(); + + // + //#region Getters ... + // + get href() { + return this.platformLocation.href; + } + + get protocol() { + return this.platformLocation.protocol; + } + + get hostname() { + return this.platformLocation.hostname; + } + + get port() { + return this.platformLocation.port; + } + + get pathname() { + return this.platformLocation.pathname; + } + + get search() { + return this.platformLocation.search; + } + + get hash() { + return this.platformLocation.hash; + } + //#endregion + //#endregion + + // + //#region Constructor ... + constructor( + public location: Location, + public platformLocation: PlatformLocation + ) { + // + // Prevent from Back Button ... + this.platformLocation.onPopState(() => { + this.backHandler.next(); + }); + } + //#endregion + + // + //#region Actions ... + pushEmptyState() { + this.pushState(null, null, this.platformLocation.href); + } + + replaceEmptyState() { + this.replaceState(null, null, this.platformLocation.href); + } + + getBaseHrefFromDOM() { + return this.platformLocation.getBaseHrefFromDOM(); + } + + getState() { + return this.platformLocation.getState(); + } + + replaceState(state: any, title: string, url: string) { + this.platformLocation.replaceState(state, title, url); + } + + pushState(state: any, title: string, url: string) { + this.platformLocation.pushState(state, title, url); + } + + forward() { + this.location.forward(); + } + + back() { + this.location.back(); + } + //#endregion + + // + //#region Private ... + //#endregion +} diff --git a/projects/x-framework-services/src/lib/x-http-interceptor.service.ts b/projects/x-framework-services/src/lib/x-http-interceptor.service.ts new file mode 100644 index 0000000..8d40fea --- /dev/null +++ b/projects/x-framework-services/src/lib/x-http-interceptor.service.ts @@ -0,0 +1,86 @@ +import { + HttpEvent, + HttpRequest, + HttpHandler, + HttpInterceptor, + HttpErrorResponse, +} from '@angular/common/http'; +import { Observable, throwError } from 'rxjs'; +import { Injectable, Inject } from '@angular/core'; +import { finalize, catchError } from 'rxjs/operators'; +import { XManagerService } from './x-manager.service'; +import { XBaseService, getErrorException } from 'x-framework-core'; +import { X_FRAMEWORK_SERVICES_CONFIG } from './tokens/x-injectable-tokens'; +import { XFrameworkServicesConfig } from './config/x-framework-services.config'; + +@Injectable({ + providedIn: 'root', +}) +export class XHttpInterceptorService + extends XBaseService + implements HttpInterceptor { + // + constructor( + @Inject(X_FRAMEWORK_SERVICES_CONFIG) config: XFrameworkServicesConfig, + private managerService: XManagerService + ) { + super(config); + } + + intercept( + req: HttpRequest, + next: HttpHandler + ): Observable> { + return this.handleInterceptRequest(req, next); + } + + handleInterceptRequest( + req: HttpRequest, + next: HttpHandler + ): Observable> { + // + // Apply Error Handler ... + const handledErrors = this.applyErrorHandler(next.handle(req)); + + // + // Apply Finally Handler ... + const handledFinally = this.applyFinally(handledErrors); + + // + // Final Level ... + const result = handledFinally; + + // + return result; + } + + // + //#region Actions / Handlers ... + applyFinally(current: Observable>) { + // + return current.pipe( + finalize(() => { + // + // Collection of instructions after finish or error ... + this.managerService.loading = false; + }) + ); + } + + applyErrorHandler(current: Observable>) { + return current.pipe( + catchError((error: HttpErrorResponse) => { + // + this.managerService.loading = true; + const mException = getErrorException(error.error); + + // + this.managerService.error = mException; + + // + return throwError(error); + }) + ); + } + //#endregion +} diff --git a/projects/x-framework-services/src/lib/x-identifier.service.ts b/projects/x-framework-services/src/lib/x-identifier.service.ts new file mode 100644 index 0000000..c5a783f --- /dev/null +++ b/projects/x-framework-services/src/lib/x-identifier.service.ts @@ -0,0 +1,113 @@ +import { Injectable } from '@angular/core'; +import { XBaseService } from 'x-framework-core'; + +@Injectable({ + providedIn: 'root', +}) +export class XIdentifierService extends XBaseService { + // + //#region Props ... + private readonly idStartKey = '_['; + private readonly idEndKey = ']'; + //#endregion + + /** + * check if task name contains id ... + * + * @param key the task name ... + */ + hasIdentifier(key: string | any, forceAppName = false) { + // + if (!key) { + return false; + } + + // + const mKey = key.toString() as string; + const result = + mKey.includes(this.idStartKey) && mKey.includes(this.idEndKey); + + // + return forceAppName + ? result && mKey.includes(this.config.appInstanceName) + : result; + } + + /** + * retrieve and extract the raw task name from a task name ... + * + * @param key the task name ... + */ + getName(key: string | any) { + // + if (!key) { + return ''; + } + + // + const mKey = key.toString() as string; + + // + if (!this.hasIdentifier(mKey)) { + return mKey; + } + + // + const startIndex = mKey.indexOf(this.idStartKey); + let result = mKey.substr(0, startIndex); + result = result.replace(this.idStartKey, ''); + + // + return result; + } + + /** + * retrieve and extract the id inside a task name ... + * + * @param key the task name ... + */ + getIdentifier(key: string | any, cleanAppName = true) { + // + // Validate Args ... + if (!key || !this.hasIdentifier(key)) { + return ''; + } + + // + // Task Id ... + let result = key.toString() as string; + + // + // Validating Info ... + let index = result.indexOf(this.idStartKey); + + // + result = result.substr(index, result.length); + result = result.replace(this.idStartKey, ''); + + // + index = result.indexOf(this.idEndKey); + result = result.substring(0, index); + + // + if (cleanAppName) { + result = result.replace(this.config.appInstanceName, ''); + } + + // + // Return Result ... + return result; + } + + /** + * prepare a Task With ID and return ... + * + * @param key the Task Name ... + * @param id th ID ... + */ + setIdentifier(key: string | any, id: string | any, containsAppName = false) { + return `${key.toString()}${this.idStartKey}${ + containsAppName ? this.config.appInstanceName : '' + }${id.toString()}${this.idEndKey}`; + } +} diff --git a/projects/x-framework-services/src/lib/x-localization.service.ts b/projects/x-framework-services/src/lib/x-localization.service.ts new file mode 100644 index 0000000..138ee49 --- /dev/null +++ b/projects/x-framework-services/src/lib/x-localization.service.ts @@ -0,0 +1,290 @@ +import { + XParam, + getVal, + XLocale, + isNumber, + formatDate, + XException, + Exceptions, + fromUtcDate, + XBaseService, + XResourceIDs, + GetDateProps, + XExceptionIDs, + throwException, + isValidException, + XTranslationItem, + isNullOrUndefined, + getErrorException, + XLanguageDirection, + XResourceIdentifier, + XLanguageDescriptor, + getErrorDateFormatString, + getDateOfBirthDateFormatString, +} from 'x-framework-core'; +import { Injectable, Inject } from '@angular/core'; +import { X_FRAMEWORK_SERVICES_CONFIG } from './tokens/x-injectable-tokens'; +import { XFrameworkServicesConfig } from './config/x-framework-services.config'; + +@Injectable({ + providedIn: 'root', +}) +export class XLocalizationService extends XBaseService { + // + private LANGUAGEDESCRIPTOR: XLanguageDescriptor; + private AVAILABLELANGUAGES: XLanguageDescriptor[]; + + // + constructor( + @Inject(X_FRAMEWORK_SERVICES_CONFIG) config: XFrameworkServicesConfig + ) { + super(config); + + // + // Inject Loacle and set Application Locale and Direction based on it ... + this.AVAILABLELANGUAGES = config.availableLanguages; + this.LANGUAGEDESCRIPTOR = config.availableLanguages.find( + (al) => al.locale === config.defaultLanguage + ); + window[XParam.Language] = this.LANGUAGEDESCRIPTOR.locale; + } + + // + //#region Properties ... + /** + * Current System Locale ... + */ + get locale() { + return this.languageDescriptor.locale; + } + + /** + * Current Locale UI Direction ... + */ + get direction(): XLanguageDirection { + return this.languageDescriptor.direction; + } + + /** + * current available language + */ + get language() { + return this.languageDescriptor.language; + } + + /** + * get Current Locale Name ... + */ + get name() { + return this.languageDescriptor.name; + } + + /** + * get Current Locale Language Descriptor Object ... + */ + get languageDescriptor() { + return this.LANGUAGEDESCRIPTOR; + } + + /** + * get Available Languages ... + */ + get availableLanguages() { + return this.AVAILABLELANGUAGES; + } + //#endregion + + /** + * change current language ... + * + * @param locale name of available language + */ + changeLocale(locale: XLocale) { + // + const languageDescriptor = this.languageDescriptorOfLocale(locale); + if (!languageDescriptor) { + throwException(XExceptionIDs.InvalidConfiguration); + } + + // + window[XParam.Language] = locale; + this.LANGUAGEDESCRIPTOR = languageDescriptor; + } + + /** + * Find specific Language Descriptor ... + */ + languageDescriptorOfLocale(locale: XLocale) { + // + return this.availableLanguages.find((al) => al.locale === locale); + } + + /** + * check an id exists in resources or not + */ + isExistsId(id: XResourceIdentifier) { + // + const key = getVal(XResourceIDs, id) || id; + + // + return ( + this.languageDescriptor.resources.findIndex((tr) => tr.id === key) > -1 + ); + } + + /** + * check an id exists in resources of specific locale or not + */ + isExistsIdOfLocale(locale: XLocale, id: XResourceIdentifier) { + // + const key = getVal(XResourceIDs, id) || id; + + // + return ( + this.languageDescriptorOfLocale(locale).resources.findIndex( + (tr) => tr.id === key + ) > -1 + ); + } + + /** + * get translation item of current language + */ + getTranslationItem(id: XResourceIdentifier): XTranslationItem { + // + const key = getVal(XResourceIDs, id) || id; + + // + return this.languageDescriptor.resources.find((tr) => tr.id === key); + } + + /** + * get translation item of specific language + */ + getTranslationItemOfLocale(locale: XLocale, id: XResourceIdentifier) { + // + const key = getVal(XResourceIDs, id) || id; + + // + const selectedLocale = this.languageDescriptorOfLocale(locale); + + // + if (!selectedLocale || !this.isExistsIdOfLocale(locale, key)) { + throwException(XExceptionIDs.NotFound); + } + + // + return selectedLocale.resources.find((tr) => tr.id === key); + } + + /** + * get Value of specific id in Current Locale + */ + getValue(id: XResourceIdentifier) { + // + if (!this.isExistsId(id)) { + return null; + } + + // + return this.getTranslationItem(id).value; + } + + /** + * get Value of specific id in specific Locale + */ + getValueOfLocale(locale: XLocale, id: XResourceIdentifier) { + // + if (!this.isExistsIdOfLocale(locale, id)) { + return null; + } + + // + return this.getTranslationItemOfLocale(locale, id).value; + } + + /** + * get coresponding translated value of errors + */ + getErrorMessage(err: any, locale?: XLocale) { + // + if (!locale) { + locale = this.locale; + } + + // + let exception: XException; + + // + if (isValidException(err)) { + exception = err as XException; + } else { + exception = getErrorException(err); + } + + // + if (exception.id && isNumber(exception.id)) { + // + const exceptionID = `ex_${exception.id}`; + return this.getValueOfLocale(locale, exceptionID); + } + + // + return this.getValueOfLocale(locale, 'general_error'); + } + + /** + * get and format date + */ + getDate(props?: GetDateProps): string { + // + if (!props) { + props = {}; + } + + // + if (!props.locale) { + props.locale = this.locale; + } + // + if (!props.date) { + props.date = new Date(); + } + // + if (!props.dateFormat) { + props.dateFormat = getDateOfBirthDateFormatString(props.locale); + } + // + if (!props.toUtc) { + props.toUtc = false; + } + // + return formatDate(props.date, props.locale, props.dateFormat, props.toUtc); + } + + /** + * combine some resources and represent single joined string ... + * + * @param locale translated dest locale ... + * @param joinString the string which used to join translated parts ... + * @param resources an array of translation resources ... + */ + combine( + locale?: XLocale, + joinString: string = ' ', + ...resources: XResourceIdentifier[] + ) { + // + if (isNullOrUndefined(locale)) { + locale = this.locale; + } + + // + const result = resources + .map((r) => this.getValueOfLocale(locale, r)) + .join(joinString); + + // + return result; + } +} diff --git a/projects/x-framework-services/src/lib/x-manager-service-partial/x-manager-base.service.ts b/projects/x-framework-services/src/lib/x-manager-service-partial/x-manager-base.service.ts new file mode 100644 index 0000000..82aa9a4 --- /dev/null +++ b/projects/x-framework-services/src/lib/x-manager-service-partial/x-manager-base.service.ts @@ -0,0 +1,48 @@ +import { Location } from '@angular/common'; +import { XBaseService } from 'x-framework-core'; +import { Platform } from '@angular/cdk/platform'; +import { HttpClient } from '@angular/common/http'; +import { Inject, Injectable } from '@angular/core'; +import { XDialogService } from '../x-dialog.service'; +import { XHistoryService } from '../x-history.service'; +import { XStorageService } from '../x-storage.service'; +import { XSecurityService } from '../x-security.service'; +import { Router, ActivatedRoute } from '@angular/router'; +import { XSettingsService } from '../x-settings.service'; +import { MenuController, NavController } from '@ionic/angular'; +import { XGeolocationService } from '../x-geolocation.service'; +import { XTaskManagerService } from '../x-task-manager.service'; +import { XNotificationService } from '../x-notification.service'; +import { XLocalizationService } from '../x-localization.service'; +import { XThemeManagerService } from '../x-theme-manager.service'; +import { XSecureStorageService } from '../x-secure-storage.service'; +import { X_FRAMEWORK_SERVICES_CONFIG } from '../tokens/x-injectable-tokens'; +import { XFrameworkServicesConfig } from '../config/x-framework-services.config'; + +@Injectable() +export abstract class XManagerBaseService extends XBaseService { + constructor( + @Inject(X_FRAMEWORK_SERVICES_CONFIG) + public config: XFrameworkServicesConfig, + public router: Router, + public platform: Platform, + public location: Location, + public httpClient: HttpClient, + public dialogService: XDialogService, + public menuController: MenuController, + public activatedRoute: ActivatedRoute, + public historyService: XHistoryService, + public storageService: XStorageService, + public securityService: XSecurityService, + public settingsService: XSettingsService, + public navigationController: NavController, + public geoLocationService: XGeolocationService, + public taskManagerService: XTaskManagerService, + public themeManagerService: XThemeManagerService, + public notificationService: XNotificationService, + public localizationService: XLocalizationService, + public secureStorageService: XSecureStorageService + ) { + super(config); + } +} diff --git a/projects/x-framework-services/src/lib/x-manager-service-partial/x-manager-final.service.ts b/projects/x-framework-services/src/lib/x-manager-service-partial/x-manager-final.service.ts new file mode 100644 index 0000000..7a01da5 --- /dev/null +++ b/projects/x-framework-services/src/lib/x-manager-service-partial/x-manager-final.service.ts @@ -0,0 +1,3 @@ +import { XManagerRouteService } from './x-manager-route.service'; + +export abstract class XManagerFinalService extends XManagerRouteService {} diff --git a/projects/x-framework-services/src/lib/x-manager-service-partial/x-manager-route.service.ts b/projects/x-framework-services/src/lib/x-manager-service-partial/x-manager-route.service.ts new file mode 100644 index 0000000..5d6ddcd --- /dev/null +++ b/projects/x-framework-services/src/lib/x-manager-service-partial/x-manager-route.service.ts @@ -0,0 +1,399 @@ +import { + keys, + XPage, + toArray, + fromJson, + hasChild, + XRouterTarget, + XOneOrManyType, + XParamIdentifier, + isNullOrEmptyString, +} from 'x-framework-core'; +import { XManagerUIStateService } from './x-manager-ui-state.service'; +import { Params, UrlTree, ParamMap, NavigationExtras } from '@angular/router'; + +export abstract class XManagerRouteService extends XManagerUIStateService { + // + //#region Props ... + // + public routeDataChanged$ = this.router.routerState.root.data; + public routeParmChanged$ = this.router.routerState.root.params; + public routeQueryParmChanged$ = this.router.routerState.root.queryParams; + //#endregion + + // + //#region Route Related Actions ... + /** + * some times we had to force change location ... + * + * @param route the specified route + */ + changeLocation(route?: string) { + window.location.href = this.getFullUrl(route); + } + + /** + * retrieve base url (Origin) address + */ + getBaseUrl() { + return window.location.origin; + } + + /** + * retrieve current route + */ + getRoute() { + return this.router && this.router.url ? this.router.url : '/'; + } + + /** + * merge several parts of a url ... + * + * @param route an instance of url tree array ... + */ + mergeRoutes(route: XOneOrManyType) { + // + if (!hasChild(route)) { + return '/'; + } + + // + try { + return toArray(route).join('/').replace('//', '/'); + } catch {} + + // + return '/'; + } + + /** + * retrieve page route ... + * + * @param page an instance of XPage ... + */ + getPageRoute(page: XPage) { + // + if (!page || !hasChild(page.route)) { + return null; + } + + // + return this.mergeRoutes(page.route); + } + + /** + * generate and retrieve the fully qualified URL address of + * specified route + * + * @param route the selected app route + */ + getFullUrl(route?: string) { + // + const mBase = this.getBaseUrl().endsWith('/') + ? this.getBaseUrl() + : `${this.getBaseUrl()}/`; + const mRoute = !route + ? '' + : !route.startsWith('/') + ? route + : route.substring(1); + + // + return `${mBase}${mRoute}`; + } + + /** + * navigate to specific location ... + * + * @param commands url or url tree parts which required to navigate + * @param extras the navigation params or options + */ + navigate(commands: any[], extras?: NavigationExtras) { + return this.router.navigate(commands, extras); + } + + /** + * navigate to specific location ... + * + * @param commands url or url tree parts which required to navigate + * @param extras the navigation params or options + * @param replaceUrl determines the url replaced or not + */ + navigateReplace( + commands: any[], + extras?: NavigationExtras, + replaceUrl: boolean = true + ) { + return this.router.navigate(commands, { ...extras, replaceUrl }); + } + + /** + * navigate to specific location by it's url ... + * + * @param url specific url address for navigating + * @param extras the navigation params or options + */ + navigateByUrl(url: string | UrlTree, extras?: NavigationExtras) { + return this.router.navigate([url], extras); + } + + /** + * navigate to specific location by it's url ... + * + * @param url specific url address for navigating + * @param extras the navigation params or options + * @param replaceUrl determines the url replaced or not + */ + navigateByUrlReplace( + url: string | UrlTree, + extras?: NavigationExtras, + replaceUrl: boolean = true + ) { + return this.router.navigate([url], { ...extras, replaceUrl }); + } + + /** + * navigate to Specific Page ... + * + * @param page an instance of XPage + * @param extras the navigation params or options + */ + navigateByPage(page: XPage, extras?: NavigationExtras) { + // + const route = this.getPageRoute(page); + if (isNullOrEmptyString(route)) { + return false; + } + + // + return this.navigateByUrl(route, extras); + } + + /** + * navigate to Specific Page ... + * + * @param page an instance of XPage + * @param extras the navigation params or options + * @param replaceUrl determines the url replaced or not + */ + navigateByPageReplace( + page: XPage, + extras?: NavigationExtras, + replaceUrl: boolean = true + ) { + // + const route = this.getPageRoute(page); + if (isNullOrEmptyString(route)) { + return false; + } + + // + return this.navigateByUrl(route, { ...extras, replaceUrl }); + } + + /** + * Applies an array of commands to the current URL tree and creates a new URL tree. + * + * When given an activated route, applies the given commands starting from the route. + * Otherwise, applies the given command starting from the root. + * + * @param commands An array of commands to apply. + * @param navigationExtras Options that control the navigation strategy. + * @returns The new URL tree. + */ + createUrlTree(commands: any[], navigationExtras?: NavigationExtras): UrlTree { + return this.router.createUrlTree(commands, navigationExtras); + } + + /** Serializes a `UrlTree` into a string */ + serializeUrl(url: UrlTree): string { + return this.router.serializeUrl(url); + } + + /** Parses a string into a `UrlTree` */ + parseUrl(url: string): UrlTree { + return this.router.parseUrl(url); + } + + /** Returns whether the url is activated */ + isActive(url: string | UrlTree, exact: boolean): boolean { + return this.router.isActive(url, exact); + } + + /** + * open specifc url ... + * + * @param url URL address + * @param target link taget + */ + openUrl(url: string, target: XRouterTarget = XRouterTarget.BLANK) { + // + if (isNullOrEmptyString(url)) { + return; + } + + // + const link = document.createElement('a'); + link.target = target; + link.href = url; + + // + link.click(); + link.remove(); + } + //#endregion + + // + //#region Handling Query Param actions ... + /** + * change page Query Params ... + * @param params Query Parameters + */ + changeQueryParams(params: Params) { + // + return this.router.navigate([], { + relativeTo: this.activatedRoute, + queryParams: params, + queryParamsHandling: 'merge', + }); + } + + clearQueryParams() { + // + return this.router.navigate([], { + relativeTo: this.activatedRoute, + queryParams: {}, + }); + } + + /** + * Get CurrentPages Query Params ... + */ + getQueryParams(): Params { + return this.router.routerState.snapshot.root.queryParams; + } + + getQueryParamMap(): ParamMap { + return this.router.routerState.snapshot.root.queryParamMap; + } + + isContainQueryParam(param: XParamIdentifier) { + return this.getQueryParamMap().has(param); + } + + getQueryParamVal(param: XParamIdentifier) { + // + if (!this.isContainQueryParam(param)) { + return null; + } + + // + return this.getQueryParamMap().get(param); + } + + getQueryParamValOfType(param: XParamIdentifier) { + // + if (!this.isContainQueryParam(param)) { + return null; + } + + // + return fromJson(this.getQueryParamMap().get(param)); + } + //#endregion + + // + //#region Handling Param actions ... + /** + * Get CurrentPages Query Params ... + */ + getParams(): Params { + return this.router.routerState.snapshot.root.params; + } + + getParamMap(): Params { + return this.router.routerState.snapshot.root.paramMap; + } + + isContainParam(param: XParamIdentifier) { + return this.getParamMap().has(param); + } + + changeParam(params: Params) { + // + return this.router.navigate(['', params], { + relativeTo: this.activatedRoute, + replaceUrl: true, + }); + } + + getParamVal(param: XParamIdentifier) { + // + if (!this.isContainParam(param)) { + return null; + } + + // + return this.getParamMap().get(param); + } + + getParamValOfType(param: XParamIdentifier) { + // + if (!this.isContainParam(param)) { + return null; + } + + // + return fromJson(this.getParamMap().get(param)); + } + //#endregion + + // + //#region State Handling ... + /** + * change page state ... + * @param params State Parameters + */ + changeState(params: Params) { + // + return this.router.navigate([], { + relativeTo: this.activatedRoute, + replaceUrl: true, + state: params, + }); + } + + getStates(): Params { + return this.router.getCurrentNavigation().extras.state || {}; + } + + isContainState(param: XParamIdentifier) { + // + const paramKeys = keys(this.getStates()); + const result = paramKeys.includes(param); + + // + return result; + } + + getStateVal(param: XParamIdentifier) { + // + if (!this.isContainState(param)) { + return null; + } + + // + return this.getStates()[param] || null; + } + + getStateValOfType(param: XParamIdentifier) { + // + if (!this.isContainState(param)) { + return null; + } + + // + return fromJson(this.getStateVal(param)) || null; + } + //#endregion +} diff --git a/projects/x-framework-services/src/lib/x-manager-service-partial/x-manager-ui-state.service.ts b/projects/x-framework-services/src/lib/x-manager-service-partial/x-manager-ui-state.service.ts new file mode 100644 index 0000000..08c5d8b --- /dev/null +++ b/projects/x-framework-services/src/lib/x-manager-service-partial/x-manager-ui-state.service.ts @@ -0,0 +1,185 @@ +import { + XLocale, + XUiEvent, + notValue, + XException, + XLanguageDirection, + isNullOrEmptyString, +} from 'x-framework-core'; +import { Subject, of } from 'rxjs'; +import { filter, concatMap } from 'rxjs/operators'; +import { XManagerBaseService } from './x-manager-base.service'; + +export abstract class XManagerUIStateService extends XManagerBaseService { + // + //#region props ... + private IS_LOADING: boolean; + private UI_DISABLED: boolean; + private ERROR: XException = null; + + // + public uiStateChanged = new Subject(); + + // + public isMobileUi$ = this.settingsService.isMobile$; + public isNotMobileUi$ = notValue(this.settingsService.isMobile$); + + // + public isLoading$ = this.uiStateChanged.asObservable().pipe( + filter((uiState) => + [XUiEvent.StartLoading, XUiEvent.StopLoading].includes(uiState) + ), + concatMap((state) => { + return of(state === XUiEvent.StartLoading ? true : false); + }) + ); + //#endregion + + // + //#region Getter(s) ... + set uiDisabled(v: boolean) { + // + if (v === this.UI_DISABLED) { + return; + } + + this.UI_DISABLED = v; + this.uiStateChanged.next(v ? XUiEvent.Disabled : XUiEvent.Enabled); + } + + // + get uiDisabled() { + return this.UI_DISABLED; + } + + // + set loading(v: boolean) { + // + if (v === this.IS_LOADING) { + return; + } + + // + this.IS_LOADING = v; + this.uiStateChanged.next(v ? XUiEvent.StartLoading : XUiEvent.StopLoading); + } + + // + get isLoading() { + return this.IS_LOADING; + } + + // + set error(exception: XException | null) { + // + if (exception === this.ERROR) { + return; + } + + // + this.ERROR = exception; + + // + if (exception) { + // + if (this.loading) { + this.loading = false; + } + + // + this.uiDisabled = true; + } else { + // + this.uiDisabled = false; + } + } + + // + get error() { + return this.ERROR; + } + + // + get hasError() { + return !!this.ERROR; + } + + // + get uiDirection(): XLanguageDirection { + return this.settingsService.uiDirection; + } + + // + get currentLanguage() { + return this.localizationService.language; + } + + // + get currentLocale(): XLocale { + return this.localizationService.locale; + } + + // + get isRtl() { + return this.settingsService.uiDirection === 'rtl'; + } + + // + get isLtr() { + return this.settingsService.uiDirection === 'ltr'; + } + //#endregion + + // + //#region UI Actions ... + // + public dispatchResizeEvent() { + // + window.dispatchEvent(new Event('resize')); + setTimeout(() => window.dispatchEvent(new Event('resize')), 500); + } + + /** + * save a text value to clipboard ... + * + * @param value the content which reuired to save to clipboard ... + */ + public saveToClipboard(value: string) { + // + if (isNullOrEmptyString(value)) { + return false; + } + + // + if ( + document.queryCommandSupported && + document.queryCommandSupported('copy') + ) { + // + const textarea = document.createElement('textarea'); + textarea.textContent = value; + // Prevent scrolling to bottom of page in Microsoft Edge. + textarea.style.position = 'fixed'; + document.body.appendChild(textarea); + textarea.select(); + + // + try { + // Security exception may be thrown by some browsers. + return document.execCommand('copy'); + } catch (ex) { + return false; + } finally { + document.body.removeChild(textarea); + } + } + + // + return false; + } + //#endregion + + // + //#region Private ... + //#endregion +} diff --git a/projects/x-framework-services/src/lib/x-manager.service.ts b/projects/x-framework-services/src/lib/x-manager.service.ts new file mode 100644 index 0000000..6424138 --- /dev/null +++ b/projects/x-framework-services/src/lib/x-manager.service.ts @@ -0,0 +1,72 @@ +import { Location } from '@angular/common'; +import { Platform } from '@angular/cdk/platform'; +import { HttpClient } from '@angular/common/http'; +import { Inject, Injectable } from '@angular/core'; +import { XDialogService } from './x-dialog.service'; +import { XStorageService } from './x-storage.service'; +import { XHistoryService } from './x-history.service'; +import { XSecurityService } from './x-security.service'; +import { XSettingsService } from './x-settings.service'; +import { Router, ActivatedRoute } from '@angular/router'; +import { XIdentifierService } from './x-identifier.service'; +import { XGeolocationService } from './x-geolocation.service'; +import { XTaskManagerService } from './x-task-manager.service'; +import { MenuController, NavController } from '@ionic/angular'; +import { XNotificationService } from './x-notification.service'; +import { XLocalizationService } from './x-localization.service'; +import { XThemeManagerService } from './x-theme-manager.service'; +import { XSecureStorageService } from './x-secure-storage.service'; +import { X_FRAMEWORK_SERVICES_CONFIG } from './tokens/x-injectable-tokens'; +import { XFrameworkServicesConfig } from './config/x-framework-services.config'; +import { XManagerFinalService } from './x-manager-service-partial/x-manager-final.service'; + +@Injectable({ + providedIn: 'root', +}) +export class XManagerService extends XManagerFinalService { + constructor( + @Inject(X_FRAMEWORK_SERVICES_CONFIG) + public config: XFrameworkServicesConfig, + public router: Router, + public platform: Platform, + public location: Location, + public httpClient: HttpClient, + public dialogService: XDialogService, + public menuController: MenuController, + public activatedRoute: ActivatedRoute, + public historyService: XHistoryService, + public storageService: XStorageService, + public securityService: XSecurityService, + public settingsService: XSettingsService, + public navigationController: NavController, + public identifierService: XIdentifierService, + public geoLocationService: XGeolocationService, + public taskManagerService: XTaskManagerService, + public themeManagerService: XThemeManagerService, + public notificationService: XNotificationService, + public localizationService: XLocalizationService, + public secureStorageService: XSecureStorageService + ) { + super( + config, + router, + platform, + location, + httpClient, + dialogService, + menuController, + activatedRoute, + historyService, + storageService, + securityService, + settingsService, + navigationController, + geoLocationService, + taskManagerService, + themeManagerService, + notificationService, + localizationService, + secureStorageService + ); + } +} diff --git a/projects/x-framework-services/src/lib/x-notification.service.ts b/projects/x-framework-services/src/lib/x-notification.service.ts new file mode 100644 index 0000000..cd46101 --- /dev/null +++ b/projects/x-framework-services/src/lib/x-notification.service.ts @@ -0,0 +1,366 @@ +import { + XColor, + isNumber, + XActionType, + XValidators, + XResourceIDs, + XBaseService, + XNotificationParams, + XTypedNotificationParams, + XErrorNotificationParams, +} from 'x-framework-core'; +import { Howl } from 'howler'; +import { ToastOptions } from '@ionic/core'; +import { ToastController } from '@ionic/angular'; +import { Injectable, Inject } from '@angular/core'; +import { XLocalizationService } from './x-localization.service'; +import { X_FRAMEWORK_SERVICES_CONFIG } from './tokens/x-injectable-tokens'; +import { XFrameworkServicesConfig } from './config/x-framework-services.config'; + +@Injectable({ + providedIn: 'root', +}) +export class XNotificationService extends XBaseService { + // + private NOTIFICATION_IDS: string[] = []; + + // + private userNotifiCationSound: Howl; + private captureNotifiCationSound: Howl; + private systemNotifiCationSound: Howl; + + // + public notificationDelay: number; + + constructor( + @Inject(X_FRAMEWORK_SERVICES_CONFIG) config: XFrameworkServicesConfig, + private localizationService: XLocalizationService, + private toastController: ToastController + ) { + // + super(config); + + // + const userActionConfig = config.notificationAudioSources.find( + (as) => as.type === XActionType.User + ); + const systemActionConfig = config.notificationAudioSources.find( + (as) => as.type === XActionType.System + ); + const captureActionConfig = config.notificationAudioSources.find( + (as) => as.type === XActionType.Capture + ); + + // + this.notificationDelay = config.defaultNotificationDelay; + + // + this.userNotifiCationSound = new Howl({ + src: [userActionConfig.resourcePath], + }); + this.captureNotifiCationSound = new Howl({ + src: [captureActionConfig.resourcePath], + }); + this.systemNotifiCationSound = new Howl({ + src: [systemActionConfig.resourcePath], + }); + } + + /** + * play sound based on type + */ + playSound(type: XActionType = XActionType.System) { + // + switch (type) { + // + case XActionType.Capture: + this.playCaptureNotification(); + break; + + // + case XActionType.User: + this.playUserNotification(); + break; + + // + default: + case XActionType.System: + this.playSystemNotification(); + break; + } + } + + /** + * play system sound + */ + playSystemNotification() { + // + if (this.systemNotifiCationSound.playing()) { + return; + } + + // + this.systemNotifiCationSound.play(); + } + + /** + * play user sound + */ + playUserNotification() { + // + if (this.userNotifiCationSound.playing()) { + return; + } + + // + this.userNotifiCationSound.play(); + } + + /** + * play Capture sound + */ + playCaptureNotification() { + // + if (this.captureNotifiCationSound.playing()) { + return; + } + + // + this.captureNotifiCationSound.play(); + } + + // + //#region Toas Related ... + /** + * create a notification object ... + * + * @param opt notification options + */ + createNotification(props?: XNotificationParams) { + const toastOpt = this.prepareToastOptions(props); + return this.toastController.create(toastOpt); + } + + /** + * create and present notification ... + * + * @param opt notification options + */ + async presentNotification(props?: XNotificationParams) { + // + const mProps = props ? props : this.getDefaultNotificationOptions(); + + // + const notification = await this.createNotification(mProps); + this.NOTIFICATION_IDS.push(notification.id); + + // + notification.onDidDismiss().then(() => { + if (mProps.onDidDismiss) { + mProps.onDidDismiss(); + this.NOTIFICATION_IDS = this.NOTIFICATION_IDS.filter( + (id) => id === notification.id + ); + } + }); + + // + notification.onWillDismiss().then(() => { + if (mProps.onWillDismiss) { + mProps.onWillDismiss(); + } + }); + + // + notification.present(); + + // + if (props && props.playSound) { + this.playSound(props.type); + } + } + + async presentXNotification(props: XTypedNotificationParams) { + // + // Validate Args ... + XValidators.validateNotNull(props); + + // + const isResourceID = isNumber(props.message); + const mMessage = isResourceID + ? this.localizationService.getValue(props.message as XResourceIDs) + : (props.message as string); + const defOpts = this.getDefaultNotificationOptions(); + const mProps = { + ...defOpts, + ...props, + opt: { + ...defOpts.opt, + ...props.opt, + message: mMessage, + }, + }; + + // + await this.presentNotification(mProps); + } + + /** + * present Info notification + * + * @param props Notification Properties + */ + async presentInfoNotification( + props: XTypedNotificationParams + ): Promise { + await this.presentXNotification({ + ...props, + opt: { + ...props.opt, + color: XColor.Tertiary, + }, + }); + } + + /** + * present Success Notification + * + * @param props Notification Properties + */ + async presentSuccessNotification( + props: XTypedNotificationParams + ): Promise { + await this.presentXNotification({ + ...props, + opt: { + ...props.opt, + color: XColor.Success, + }, + }); + } + + /** + * present Default Success Notification + */ + async presentDefaultSuccessNotification( + dissmissHandler?: () => void + ): Promise { + // + const mProps: XTypedNotificationParams = { + message: XResourceIDs.success_msg, + dissmissable: true, + onWillDismiss: dissmissHandler, + }; + + await this.presentSuccessNotification(mProps); + } + + /** + * present warning notifications + * + * @param props Notification Properties + */ + async presentWarningNotification( + props: XTypedNotificationParams + ): Promise { + await this.presentXNotification({ + ...props, + opt: { + ...props.opt, + color: XColor.Warning, + }, + }); + } + + /** + * present error notification + * + * @param props Notification Properties + */ + async presentDangerNotification( + props: XTypedNotificationParams + ): Promise { + await this.presentXNotification({ + ...props, + opt: { + ...props.opt, + color: XColor.Danger, + }, + }); + } + + /** + * present an Exception or Error Message ... + * + * @param props Notification Properties + */ + async presentErrorNotification( + props: XErrorNotificationParams + ): Promise { + // + const mMessage = this.localizationService.getErrorMessage(props.exception); + await this.presentXNotification({ + ...props, + message: mMessage, + dissmissable: props.dissmissable, + opt: { + ...props.opt, + color: XColor.Danger, + }, + }); + } + + async closeAllNotifications() { + this.NOTIFICATION_IDS.forEach(async (id) => { + try { + await this.toastController.dismiss(null, null, id); + } catch {} + }); + } + + /** + * combines default notification options by + * given notification options ... + * + * @param opt notification options + */ + private prepareToastOptions(props?: XNotificationParams): ToastOptions { + // + if (!props) { + props = this.getDefaultNotificationOptions(); + } + + // + // Handle Dissmissable + const mDuration = props.dissmissable ? props.opt.duration : 0; + + // + const result = { + ...props.opt, + duration: mDuration, + }; + + // + return result; + } + + /** + * generate default notification options ... + */ + private getDefaultNotificationOptions(): XNotificationParams { + return { + playSound: false, + type: XActionType.System, + dissmissable: true, + opt: { + color: XColor.Tertiary, + position: 'bottom', + duration: this.notificationDelay, + // , + // showCloseButton: true, + // closeButtonText: this.localizationService.getValue(XResourceIDs.dismiss) + }, + }; + } + //#endregion +} diff --git a/projects/x-framework-services/src/lib/x-secure-storage.service.ts b/projects/x-framework-services/src/lib/x-secure-storage.service.ts new file mode 100644 index 0000000..850f995 --- /dev/null +++ b/projects/x-framework-services/src/lib/x-secure-storage.service.ts @@ -0,0 +1,137 @@ +import { + fromJson, + XStorageItem, + XExceptionIDs, + throwException, + isNullOrUndefined +} from 'x-framework-core'; +import { Injectable, Inject } from '@angular/core'; +import { XStorageService } from './x-storage.service'; +import { XSecurityService } from './x-security.service'; +import { X_FRAMEWORK_SERVICES_CONFIG } from './tokens/x-injectable-tokens'; +import { XFrameworkServicesConfig } from './config/x-framework-services.config'; + +@Injectable({ + providedIn: 'root' +}) +export class XSecureStorageService extends XStorageService { + // + constructor( + @Inject(X_FRAMEWORK_SERVICES_CONFIG) config: XFrameworkServicesConfig, + private securitySerive: XSecurityService + ) { + super(config); + } + + /** + * writing a new item in Storage ... + * + * @param key the item key + * @param data the data object which needs to save + * @param forceRewrite overwrite item if exists, the default is false, + * means if key exists throws an error ... + */ + write(key: string, data: any, forceRewrite: boolean = false) { + // + if (!forceRewrite && this.isExists(key)) { + throwException(XExceptionIDs.Duplicate); + } + + // + const theKey = this.getPreparedKey(key); + const encryptedValue = this.securitySerive.encryptWithKey( + JSON.stringify(data) + ); + + // + localStorage.setItem(theKey, encryptedValue); + } + + /** + * read an item from Storage ... + * @param key the item key + * @param forceExists determine the given key must be exists or not, true is default, + * means if key not exists throws an error ... + */ + read(key: string, forceExists: boolean = true): any | null { + // + if (forceExists && !this.isExists(key)) { + throwException(XExceptionIDs.NotFound); + } + + // + const theKey = this.getPreparedKey(key); + const item = localStorage.getItem(theKey); + if (!isNullOrUndefined(item)) { + try { + const decryptedValue = this.securitySerive.decryptWithKey(item); + return JSON.parse(decryptedValue); + } catch {} + } + + // + return null; + } + readOfType(key: string, forceExists: boolean = true): T | null { + // + if (!this.isExists(key)) { + // + if (forceExists) { + throwException(XExceptionIDs.NotFound); + } + + // + return null; + } + + // + const valStr = this.read(key, forceExists); + return fromJson(valStr); + } + + /** + * get a List of Stored items as an Array of StorageItem ... + * @param force true is defualt, means only return list of items + * from Storage which Stored by Service ... + */ + list(force: boolean = true, decrypt: boolean = false): XStorageItem[] | null { + // + if (localStorage.length === 0) { + return null; + } + + // + const items: XStorageItem[] = []; + for (let i = 0; i < localStorage.length; i++) { + // + const mKey = localStorage.key(i); + + // + const mValue = localStorage.getItem(mKey); + const mValueRes = decrypt + ? this.securitySerive.decryptWithKey(mValue) + : mValue; + + // + if (force) { + const theRawKey = this.getRawKey(mKey); + if (!isNullOrUndefined(theRawKey)) { + // + items.push({ + key: theRawKey, + value: mValueRes + }); + } + } else { + // + items.push({ + key: mKey, + value: mValueRes + }); + } + } + + // + return items; + } +} diff --git a/projects/x-framework-services/src/lib/x-security.service.ts b/projects/x-framework-services/src/lib/x-security.service.ts new file mode 100644 index 0000000..671d553 --- /dev/null +++ b/projects/x-framework-services/src/lib/x-security.service.ts @@ -0,0 +1,173 @@ +import * as CryptoJS from 'crypto-js'; +import { DomSanitizer } from '@angular/platform-browser'; +import { X_FRAMEWORK_SERVICES_CONFIG } from './tokens/x-injectable-tokens'; +import { toMd5, XBaseService, isNullOrEmptyString } from 'x-framework-core'; +import { Injectable, Inject, Sanitizer, SecurityContext } from '@angular/core'; +import { XFrameworkServicesConfig } from './config/x-framework-services.config'; + +@Injectable({ + providedIn: 'root', +}) +export class XSecurityService extends XBaseService { + // + private readonly SECRET_KEY: string; + private readonly CIPHER_KEY: string; + private readonly CIPHER_IV: string; + private readonly CIPHER_OPTION: CryptoJS.lib.CipherOption; + + // + constructor( + @Inject(X_FRAMEWORK_SERVICES_CONFIG) config: XFrameworkServicesConfig, + private sanitizer: Sanitizer, + private domSanitizer: DomSanitizer + ) { + super(config); + + // + this.SECRET_KEY = toMd5(config.secretKey); + + // + const forIv = this.SECRET_KEY.substring(0, 16); + this.CIPHER_IV = CryptoJS.enc.Utf8.parse(forIv).toString(); + + // + const forKey = this.SECRET_KEY.substring(0, 32); + this.CIPHER_KEY = CryptoJS.enc.Utf8.parse(forKey).toString(); + + // + this.CIPHER_OPTION = { + iv: CryptoJS.enc.Utf8.parse(this.CIPHER_IV), + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7, + }; + } + + encryptWithKey( + value: string, + key?: string, + options?: CryptoJS.lib.CipherOption + ) { + // + if (isNullOrEmptyString(value)) { + return null; + } + + // + if (!key) { + key = this.CIPHER_KEY; + } + + // + if (!options) { + options = this.CIPHER_OPTION; + } + + // + return CryptoJS.AES.encrypt(value, key, options).toString(); + } + + encryptWithSecret( + value: string, + secret?: string, + options?: CryptoJS.lib.CipherOption + ) { + // + if (isNullOrEmptyString(value)) { + return null; + } + + // + if (!secret) { + secret = this.SECRET_KEY; + } + + // + if (!options) { + options = this.CIPHER_OPTION; + } + + // + return CryptoJS.AES.encrypt(value, secret, options); + } + + decryptWithKey( + cipherValue: string, + key?: string, + options?: CryptoJS.lib.CipherOption + ) { + // + if (isNullOrEmptyString(cipherValue)) { + return null; + } + + // + if (!key) { + key = this.CIPHER_KEY; + } + + // + if (!options) { + options = this.CIPHER_OPTION; + } + + // + const result = CryptoJS.AES.decrypt(cipherValue, key, options).toString( + CryptoJS.enc.Utf8 + ); + + // + return result; + } + + decryptWithSecret( + cipherValue: string, + secret?: string, + options?: CryptoJS.lib.CipherOption + ) { + // + if (isNullOrEmptyString(cipherValue)) { + return null; + } + + // + if (!secret) { + secret = this.SECRET_KEY; + } + + // + if (!options) { + options = this.CIPHER_OPTION; + } + + // + return CryptoJS.AES.decrypt(cipherValue, secret, options).toString(); + } + + toMd5(value: string) { + return toMd5(value); + } + + toSafeString(value: string, context: SecurityContext = SecurityContext.NONE) { + return this.sanitizer.sanitize(context, value); + } + + toSafeHTML(value: string) { + return this.domSanitizer.bypassSecurityTrustHtml(value); + } + + toSafeResourceURL(value: string) { + return this.domSanitizer.bypassSecurityTrustResourceUrl(value); + } + + toSafeURL(value: string) { + return this.domSanitizer.bypassSecurityTrustUrl(value); + } + + toSafeScript(value: string) { + return this.domSanitizer.bypassSecurityTrustScript(value); + } + + toSafeStyle(value: string) { + return this.domSanitizer.bypassSecurityTrustStyle(value); + } +} diff --git a/projects/x-framework-services/src/lib/x-settings.service.ts b/projects/x-framework-services/src/lib/x-settings.service.ts new file mode 100644 index 0000000..2242073 --- /dev/null +++ b/projects/x-framework-services/src/lib/x-settings.service.ts @@ -0,0 +1,478 @@ +import { + isArray, + XLocale, + XDeviceInfo, + XValidators, + XOrientation, + XBaseService, + XDeviceTypes, + XOneOrManyType, + XCssProperties, + XLanguageDirection, + XSettingsStorageKey, + isNullOrEmptyString, +} from 'x-framework-core'; +import { + Breakpoints, + BreakpointState, + BreakpointObserver, +} from '@angular/cdk/layout'; +import { Observable } from 'rxjs'; +import { Md5 } from 'md5-typescript'; +import { map } from 'rxjs/operators'; +import { Platform } from '@ionic/angular'; +import { Injectable, Inject } from '@angular/core'; +import { XStorageService } from './x-storage.service'; +import { DeviceDetectorService } from 'ngx-device-detector'; +import { XLocalizationService } from './x-localization.service'; +import { Meta, Title, MetaDefinition } from '@angular/platform-browser'; +import { X_FRAMEWORK_SERVICES_CONFIG } from './tokens/x-injectable-tokens'; +import { XFrameworkServicesConfig } from './config/x-framework-services.config'; + +@Injectable({ + providedIn: 'root', +}) +export class XSettingsService extends XBaseService { + // + private PREFIX = '--'; + private DEVICE_INFO: XDeviceInfo; + private SIDE_MENU_OPENED: boolean; + + // + //#region Breakpoint Observers ... + isMobile$: Observable; + isXSmall$: Observable; + isSmall$: Observable; + isMedium$: Observable; + isLarge$: Observable; + isXLarge$: Observable; + isTablet$: Observable; + isWeb$: Observable; + isPortrait$: Observable; + isLandscape$: Observable; + orientation$: Observable; + //#endregion + + // + //#region Properties ... + // + get uiDirection(): XLanguageDirection { + return this.localizationService.direction; + } + //#endregion + + constructor( + public platform: Platform, + private meta: Meta, + private titleService: Title, + @Inject(X_FRAMEWORK_SERVICES_CONFIG) config: XFrameworkServicesConfig, + private storageService: XStorageService, + private localizationService: XLocalizationService, + private deviceService: DeviceDetectorService, + private breakPointObserver: BreakpointObserver + ) { + // + super(config); + + // + // Handle Ui Directions based on Current Locale ... + this.applyCurrentLocale(); + + // + // Generate Device Info ... + this.prepareDeviceInfo(); + + // + // define objservable ... + this.defineUiBreakPointObservers(); + + // + // Handle Metas for Theme ... + this.handleThemeMetaTags(); + + // + // Prepare Toolbar Height ... + } + + /** + * restart application ... + */ + restart() { + // + if (!window || !window.location) { + return; + } + + // + const href = window.location.href; + setTimeout(() => { + window.location.href = href; + window.location.reload(); + }, 100) + } + + /** + * change system locale ... + * + * @param locale available locale + */ + async changeLocale(locale: XLocale, refresh: boolean = false) { + // + XValidators.validateNotNull(locale); + if (locale === this.localizationService.locale) { + return; + } + + // + this.storageService.write(XSettingsStorageKey.Locale, locale, true); + this.localizationService.changeLocale(locale); + this.applyCurrentLocale(); + + // + if (refresh) { + this.restart(); + } + } + + // + //#region Met Seo Service ... + addTag(tag: MetaDefinition, forceCreation?: boolean) { + this.meta.addTag(tag, forceCreation); + } + + addTags(tags: MetaDefinition[], forceCreation?: boolean) { + this.meta.addTags(tags, forceCreation); + } + + getTag(attrSelector: string) { + this.meta.getTag(attrSelector); + } + + getTags(attrSelector: string) { + this.meta.getTags(attrSelector); + } + + updateTag(tag: MetaDefinition, attrSelector?: string) { + this.meta.updateTag(tag, attrSelector); + } + + removeTag(attrSelector: string) { + this.meta.removeTag(attrSelector); + } + + removeTagElement(meta: HTMLMetaElement) { + this.meta.removeTagElement(meta); + } + //#endregion + + // + //#region Title Service ... + /** + * set Page ( Current Document ) Title + * in Browser ... + */ + setTitle(title?: string) { + // + const appNameKey = this.config.appNameResource; + const appName = this.localizationService.getValue(appNameKey); + + // + const pageTitle = !isNullOrEmptyString(title) + ? `${appName} > ${title}` + : appName; + + // + this.titleService.setTitle(pageTitle); + } + + /** + * get the Current Document Title ... + */ + getTitle() { + return this.titleService.getTitle(); + } + //#endregion + + // + //#region Device Service ... + /** + * get Current Device Model Info + */ + get device() { + return this.DEVICE_INFO; + } + + /** + * get Current Device token + */ + get deviceToken() { + return this.DEVICE_INFO.token; + } + //#endregion + + // + //#region Css Property Handler ... + /** + * reading the CSS Property ... + * + * @param key the key of property + */ + readCssProperty(key: XCssProperties): string { + // + const bodyStyles = window.getComputedStyle(document.body); + return bodyStyles.getPropertyValue(this.prepareCssKey(key)); + } + + getCssVar(name: XCssProperties, element?: HTMLElement) { + // + const docElement = element + ? getComputedStyle(element) + : getComputedStyle(document.documentElement); + return !name || !docElement || isNullOrEmptyString(name) + ? '' + : docElement.getPropertyValue(this.prepareCssKey(name)); + } + + setCssVar( + name: XCssProperties, + value: string, + elements?: XOneOrManyType + ) { + // + if (isNullOrEmptyString(name)) { + return; + } + + // + if (!elements || !isArray(elements)) { + // + const docElement = elements + ? (elements as HTMLElement).style + : document.documentElement.style; + + // + docElement.setProperty(this.prepareCssKey(name), value); + } else { + (elements as HTMLElement[]).forEach((e) => { + e.style.setProperty(this.prepareCssKey(name), value); + }); + } + } + //#endregion + + // + //#region Theme Manager ... + + //#endregion + + // + //#region Private + private applyCurrentLocale() { + // + let storedLocale = this.localizationService.locale; + const isExistsLocale = this.storageService.isExists( + XSettingsStorageKey.Locale + ); + if (isExistsLocale) { + storedLocale = this.storageService.readOfType( + XSettingsStorageKey.Locale + ); + this.localizationService.changeLocale(storedLocale); + } + + // + this.storageService.write(XSettingsStorageKey.Locale, storedLocale, true); + + // + //#region Handle Ui Directions based on Current Locale ... + document.documentElement.lang = this.localizationService.language; + document.documentElement.dir = this.localizationService.direction; + + // + this.setTitle(); + + // + // Document CSS ... + document.body.style.setProperty( + this.prepareCssKey('ui-direction'), + this.localizationService.direction + ); + + // + const isRtl = this.localizationService.direction === 'rtl'; + document.body.style.setProperty( + this.prepareCssKey('is-rtl'), + isRtl.toString() + ); + //#endregion + } + + private prepareDeviceInfo() { + // + //#region Generate Device Info ... + // + // Reading Device Specifications ... + const deviceOs = this.deviceService.os; + const deviceBrowser = this.deviceService.browser; + const deviceOsVersion = this.deviceService.os_version; + const deviceUserAgent = this.deviceService.userAgent; + + // + // Generating Device Type ... + let mDeviceType: XDeviceTypes; + if (this.deviceService.isDesktop()) { + mDeviceType = XDeviceTypes.Desktop; + } else if (this.deviceService.isTablet()) { + mDeviceType = XDeviceTypes.Tablet; + } else if (this.deviceService.isMobile()) { + mDeviceType = XDeviceTypes.Mobile; + } else { + mDeviceType = XDeviceTypes.Unknown; + } + + // + // Generating Device Identifier and it's token ... + const deviceIdentifier = `[${deviceOs}]-[${deviceOsVersion}]-[${mDeviceType}]-[${deviceBrowser}]-[${deviceUserAgent}]`; + const deviceToken = Md5.init(deviceIdentifier); + + // + this.DEVICE_INFO = { + os: deviceOs, + browser: deviceBrowser, + osVersion: deviceOsVersion, + userAgent: deviceUserAgent, + deviceType: mDeviceType, + identifier: deviceIdentifier, + token: deviceToken, + }; + //#endregion + } + + private defineUiBreakPointObservers() { + // + //#region define objservable ... + this.isPortrait$ = this.breakPointObserver + .observe([ + Breakpoints.WebPortrait, + Breakpoints.TabletPortrait, + Breakpoints.HandsetPortrait, + ]) + .pipe(map((breakPointState: BreakpointState) => breakPointState.matches)); + + // + this.isLandscape$ = this.breakPointObserver + .observe([ + Breakpoints.WebLandscape, + Breakpoints.TabletLandscape, + Breakpoints.HandsetLandscape, + ]) + .pipe(map((breakPointState: BreakpointState) => breakPointState.matches)); + + // + this.orientation$ = this.breakPointObserver + .observe([ + Breakpoints.WebPortrait, + Breakpoints.TabletPortrait, + Breakpoints.HandsetPortrait, + Breakpoints.WebLandscape, + Breakpoints.TabletLandscape, + Breakpoints.HandsetLandscape, + ]) + .pipe( + map(() => { + if ( + this.breakPointObserver.isMatched([ + Breakpoints.WebPortrait, + Breakpoints.TabletPortrait, + Breakpoints.HandsetPortrait, + ]) + ) { + return 'portrait'; + } else { + return 'landscape'; + } + }) + ); + + // + this.isXSmall$ = this.breakPointObserver + .observe(Breakpoints.XSmall) + .pipe(map((breakPointState: BreakpointState) => breakPointState.matches)); + + // + this.isSmall$ = this.breakPointObserver + .observe(Breakpoints.Small) + .pipe(map((breakPointState: BreakpointState) => breakPointState.matches)); + + // + this.isMedium$ = this.breakPointObserver + .observe(Breakpoints.Medium) + .pipe(map((breakPointState: BreakpointState) => breakPointState.matches)); + + // + this.isLarge$ = this.breakPointObserver + .observe(Breakpoints.Large) + .pipe(map((breakPointState: BreakpointState) => breakPointState.matches)); + + // + this.isXLarge$ = this.breakPointObserver + .observe(Breakpoints.XLarge) + .pipe(map((breakPointState: BreakpointState) => breakPointState.matches)); + + // + this.isTablet$ = this.breakPointObserver + .observe(Breakpoints.Tablet) + .pipe(map((breakPointState: BreakpointState) => breakPointState.matches)); + + // + this.isWeb$ = this.breakPointObserver + .observe(Breakpoints.Web) + .pipe(map((breakPointState: BreakpointState) => breakPointState.matches)); + + // + this.isMobile$ = this.breakPointObserver + .observe('(max-width: 768px)') + .pipe(map((breakPointState: BreakpointState) => breakPointState.matches)); + //#endregion + } + + private handleThemeMetaTags() { + // + //#region Handle Metas for Theme ... + const darkPrimaryColor = this.readCssProperty('x-app-bar-color'); + this.addTag( + { + name: 'theme-color', + content: darkPrimaryColor, + }, + true + ); + this.addTag( + { + name: 'msapplication-navbutton-color', + content: darkPrimaryColor, + }, + true + ); + this.addTag( + { + name: 'apple-mobile-web-app-capable', + content: 'yes', + }, + true + ); + this.addTag( + { + name: 'apple-mobile-web-app-status-bar-style', + content: darkPrimaryColor, + }, + true + ); + //#endregion + } + + private prepareCssKey(key: XCssProperties) { + return `${this.PREFIX}${key}`; + } + //#endregion +} diff --git a/projects/x-framework-services/src/lib/x-storage.service.ts b/projects/x-framework-services/src/lib/x-storage.service.ts new file mode 100644 index 0000000..d25f143 --- /dev/null +++ b/projects/x-framework-services/src/lib/x-storage.service.ts @@ -0,0 +1,203 @@ +import { + fromJson, + XStorageItem, + XBaseService, + XExceptionIDs, + throwException, + isNullOrUndefined +} from 'x-framework-core'; +import { Injectable, Inject } from '@angular/core'; +import { X_FRAMEWORK_SERVICES_CONFIG } from './tokens/x-injectable-tokens'; +import { XFrameworkServicesConfig } from './config/x-framework-services.config'; + +@Injectable({ + providedIn: 'root' +}) +export class XStorageService extends XBaseService { + // + private keyPrefix: string; + + // + constructor( + @Inject(X_FRAMEWORK_SERVICES_CONFIG) config: XFrameworkServicesConfig + ) { + super(config); + + // + if (isNullOrUndefined(typeof Storage)) { + throwException(XExceptionIDs.StorageNotSupported); + } + + // + this.keyPrefix = `${config.appInstanceName}`; + } + + /** + * determine a key exists in storage or not ... + * @param key the item key + */ + isExists(key: string) { + // + const theKey = this.getPreparedKey(key); + const result = localStorage.getItem(theKey) !== null; + + // + return result; + } + + /** + * writing a new item in Storage ... + * + * @param key the item key + * @param data the data object which needs to save + * @param forceRewrite overwrite item if exists, the default is false, + * means if key exists throws an error ... + */ + write(key: string, data: any, forceRewrite: boolean = false) { + // + if (!forceRewrite && this.isExists(key)) { + throwException(XExceptionIDs.Duplicate); + } + + // + const theKey = this.getPreparedKey(key); + localStorage.setItem(theKey, JSON.stringify(data)); + } + + /** + * read an item from Storage ... + * @param key the item key + * @param forceExists determine the given key must be exists or not, true is default, + * means if key not exists throws an error ... + */ + read(key: string, forceExists: boolean = true): any | null { + // + if (forceExists && !this.isExists(key)) { + throwException(XExceptionIDs.NotFound); + } + + // + const theKey = this.getPreparedKey(key); + const item = localStorage.getItem(theKey); + if (!isNullOrUndefined(item)) { + try { + return JSON.parse(item); + } catch {} + } + + // + return null; + } + readOfType(key: string, forceExists: boolean = true): T | null { + // + if (!this.isExists(key)) { + // + if (forceExists) { + throwException(XExceptionIDs.NotFound); + } + + // + return null; + } + + // + const valStr = this.read(key, forceExists); + return fromJson(valStr); + } + + /** + * remove an stored item from Storage ... + * @param key the item key + * @param forceExists determine the given key must be exists or not, true is default, + * means if key not exists throws an error ... + */ + remove(key: string, forceExists: boolean = true) { + // + if (forceExists && !this.isExists(key)) { + throwException(XExceptionIDs.NotFound); + } + + // + const theKey = this.getPreparedKey(key); + localStorage.removeItem(theKey); + } + + /** + * get a List of Stored items as an Array of StorageItem ... + * @param force true is defualt, means only return list of items + * from Storage which Stored by Service ... + */ + list(force: boolean = true): XStorageItem[] | null { + // + if (localStorage.length === 0) { + return null; + } + + // + const items: XStorageItem[] = []; + for (let i = 0; i < localStorage.length; i++) { + // + const mKey = localStorage.key(i); + + // + if (force) { + const theRawKey = this.getRawKey(mKey); + if (!isNullOrUndefined(theRawKey)) { + // + items.push({ + key: theRawKey, + value: localStorage.getItem(mKey) + }); + } + } else { + // + items.push({ + key: mKey, + value: localStorage.getItem(mKey) + }); + } + } + + // + return items; + } + + /** + * get the number of items which stored in Storage ... + * @param force true is defualt, means only return number of items + * from Storage which Stored by Service ... + */ + length(force: boolean = true) { + // + if (force) { + return this.list().length; + } + + // + return localStorage.length; + } + + /** + * clear all items from Storage ... + * @param force true is defualt, means only clear items + * from Storage which Stored by Service ... + */ + clear(force: boolean = true) { + localStorage.clear(); + } + + protected getPreparedKey(key: string) { + return `${this.keyPrefix}_${key}`; + } + + protected getRawKey(key: string): string | null { + // + const baseKeySubstr = `${this.keyPrefix}_`; + if (key.indexOf(baseKeySubstr) === -1) { + return null; + } + + // + return key.replace(baseKeySubstr, ''); + } +} diff --git a/projects/x-framework-services/src/lib/x-task-manager.service.ts b/projects/x-framework-services/src/lib/x-task-manager.service.ts new file mode 100644 index 0000000..05f2734 --- /dev/null +++ b/projects/x-framework-services/src/lib/x-task-manager.service.ts @@ -0,0 +1,295 @@ +import { + XTask, + XException, + XBaseService, + XTaskPriority, + isValidException, + getErrorException, +} from 'x-framework-core'; +import { Injectable, Inject } from '@angular/core'; +import { concatMap, catchError, tap } from 'rxjs/operators'; +import { X_FRAMEWORK_SERVICES_CONFIG } from './tokens/x-injectable-tokens'; +import { Subscription, Subject, BehaviorSubject, of, throwError } from 'rxjs'; +import { XFrameworkServicesConfig } from './config/x-framework-services.config'; + +@Injectable({ + providedIn: 'root', +}) +export class XTaskManagerService extends XBaseService { + // + //#region Props ... + // + subscription: Subscription; + + // + // Notify loading ... + public allTasksFinished = new Subject(); + public loading = new BehaviorSubject(false); + + // + // notify Specific Task Start or Finished ... + public taskStart = new Subject>(); + public taskError = new Subject>(); + public taskFinished = new Subject>(); + + // + private isPaused = false; + private isStarted = false; + private tasks = new Map>(); + private taskQueue = new Subject>(); + + /** + * retrieve a Collection of Tasks which Pending to run ... + */ + get taskList() { + return this.getSortedTasksLists(); + } + + /** + * retrieve Running State of Tasks ... + */ + get paused() { + return this.isPaused; + } + //#endregion + + // + //#region Constructor ... + constructor( + @Inject(X_FRAMEWORK_SERVICES_CONFIG) config: XFrameworkServicesConfig + ) { + super(config); + + // + // Register Task Queue Runner ... + this.taskQueue.asObservable().subscribe((t) => { + this.handleTaskRun(t); + }); + + // + this.allTasksFinished.asObservable().subscribe(() => { + this.loading.next(false); + }); + } + //#endregion + + // + //#region Actions ... + /** + * determines a Tasks is in Pending List or not ... + */ + has(taskName: string) { + return this.tasks.has(taskName); + } + + /** + * delete a task from Pending List ... + */ + delete(taskName: string) { + return this.tasks.delete(taskName); + } + + /** + * add a Task to Pending List ... + */ + add(task: XTask) { + // + // Prevent Duplicate ... + if (this.has(task.name)) { + return false; + } + + // + // add Task to Pending List ... + this.tasks.set(task.name, task); + + // + // Start Running if it's not Started Before ... + if (!this.isStarted) { + this.requestNext(); + } + + // + return true; + } + + /** + * Change Specific Tasks Priority ... + */ + changePriority(taskName: string, priority: XTaskPriority) { + // + if (!this.has(taskName)) { + return; + } + + // + this.tasks.get(taskName).priority = priority; + } + + /** + * Count Pending List Tasks ... + */ + count() { + return this.tasks.size; + } + + /** + * Remove All Pending List Task ... + */ + clear() { + this.isStarted = false; + this.tasks.clear(); + } + + /** + * Pause Running of Tasks ... + */ + pause() { + this.isPaused = true; + } + + /** + * Continue Running Pending Tasks ... + */ + continue() { + // + if (!this.isPaused) { + return; + } + + // + this.isPaused = false; + this.requestNext(); + } + + /** + * Toggle Pause/Continue Tasks Running ... + */ + toggle() { + // + if (this.isPaused) { + this.continue(); + } else { + this.pause(); + } + } + //#endregion + + // + //#region Private ... + private requestNext() { + // + if (this.isPaused) { + return; + } + + // + const task = this.popTask(); + + // + if (task) { + // + this.isStarted = true; + this.taskQueue.next(task); + } else { + // + this.isStarted = false; + if (this.tasks.size === 0) { + // + this.loading.next(false); + this.allTasksFinished.next(); + } + } + } + + private popTask() { + // + const result = this.getSortedTasksLists().pop() || null; + + // + if (result) { + this.tasks.delete(result.name); + } + return result; + } + + private getSortedTasksLists() { + // + return Array.from(this.tasks.values()) + .sort((a, b) => a.priority - b.priority) + .reverse(); + } + + private applyWrapperToTask(task: XTask) { + // + return of(task).pipe( + concatMap((res) => { + // + if (res.affectLoading) { + this.loading.next(true); + } + + // + this.taskStart.next(res); + + // + return res.task.pipe( + catchError((err) => { + // + this.isStarted = false; + this.loading.next(false); + + // + res.error = getErrorException(err); + + // + if (isValidException(err.error)) { + res.error = err.error as XException; + } + + // + this.taskError.next(res); + + // + this.handleUnSubscribe(); + return throwError(res.error); + }), + tap((tRes) => { + // + this.isStarted = false; + this.loading.next(false); + + // + res.result = tRes; + this.taskFinished.next(res); + + // + this.handleUnSubscribe(); + this.requestNext(); + }) + ); + }) + ); + } + + private handleTaskRun(task: XTask) { + // + const mXTObs = this.applyWrapperToTask(task); + + // + this.subscription = mXTObs.subscribe( + (_) => {}, + (_) => {} + ); + } + + private handleUnSubscribe() { + // + if (this.subscription) { + // + this.subscription.unsubscribe(); + this.subscription = null; + } + } + //#endregion +} diff --git a/projects/x-framework-services/src/lib/x-theme-manager.service.ts b/projects/x-framework-services/src/lib/x-theme-manager.service.ts new file mode 100644 index 0000000..b55fe1b --- /dev/null +++ b/projects/x-framework-services/src/lib/x-theme-manager.service.ts @@ -0,0 +1,1216 @@ +import { + keys, + hasChild, + XThemeType, + XThemePack, + XThemeColors, + downloadFile, + XBaseService, + XExceptionIDs, + XThemeCssVars, + toNormalString, + isNullOrUndefined, + isNullOrEmptyString, + XExceptionIdentifier, + XThemeTypeIdentifier, + XThemeCssVarsIdentifier, +} from 'x-framework-core'; +import { XStorageService } from './x-storage.service'; +import { XSettingsService } from './x-settings.service'; +import { XNotificationService } from './x-notification.service'; +import { X_FRAMEWORK_SERVICES_CONFIG } from './tokens/x-injectable-tokens'; +import { Injectable, Inject, EventEmitter, ElementRef } from '@angular/core'; +import { XFrameworkServicesConfig } from './config/x-framework-services.config'; + +const XThemeManagerStorageKeys = { + CurrentTheme: 'current_theme', + CurrentThemeType: 'current_theme_type', + Themes: 'themes', +}; + +@Injectable({ + providedIn: 'root', +}) +export class XThemeManagerService extends XBaseService { + // + //#region Props ... + // + private PREFIX = '--'; + readonly ColorCssVars = Object.assign({}, XThemeCssVars); + + // + notifyChanges = new EventEmitter(); + //#endregion + + // + //#region Constructor ... + constructor( + @Inject(X_FRAMEWORK_SERVICES_CONFIG) + public config: XFrameworkServicesConfig, + private storageService: XStorageService, + private settingsService: XSettingsService, + private notificationService: XNotificationService + ) { + // + super(config); + + // + this.initializeService(); + } + //#endregion + + // + //#region Actions ... + async applyCurrentTheme(element?: ElementRef) { + // + const currentTheme = await this.getCurrentTheme(); + const isDefaultThemeExists = !isNullOrUndefined(currentTheme); + if (isDefaultThemeExists) { + // + const type = await this.getCurrentThemeType(); + await this.setTheme(currentTheme, type, false, false, element); + + // + this.notifyChanges.emit(); + } + } + + async getCurrentThemeName() { + // + const mKey = XThemeManagerStorageKeys.CurrentTheme; + + // + const isExists = this.storageService.isExists(mKey); + if (!isExists) { + return ''; + } + + // + const result = this.storageService.readOfType(mKey); + + // + return result; + } + + async setCurrentThemeName(themeName: string, notifyChanges: boolean = true) { + // + const isExists = await this.isThemeExists(themeName); + if (!isExists) { + // + if (notifyChanges) { + await this.issueFailedNotification(XExceptionIDs.NotFound); + } + + // + return false; + } + + // + const mKey = XThemeManagerStorageKeys.CurrentTheme; + + // + this.storageService.write(mKey, themeName, true); + + // + if (notifyChanges) { + // + this.notifyChanges.emit(); + await this.issueSuccessNotification(); + } + + // + return true; + } + + async getCurrentThemeType() { + // + const mKey = XThemeManagerStorageKeys.CurrentThemeType; + + // + const isExists = this.storageService.isExists(mKey); + if (!isExists) { + return XThemeType.Light; + } + + // + const result = this.storageService.readOfType(mKey); + + // + return result; + } + + async setCurrentThemeType( + themeType: XThemeTypeIdentifier, + notifyChanges: boolean = true + ) { + // + if (isNullOrEmptyString(themeType)) { + // + if (notifyChanges) { + await this.issueFailedNotification(XExceptionIDs.InvalidArgs); + } + + // + return false; + } + + // + const mKey = XThemeManagerStorageKeys.CurrentThemeType; + + // + this.storageService.write(mKey, themeType, true); + + // + if (notifyChanges) { + // + this.notifyChanges.emit(); + await this.issueSuccessNotification(); + } + + // + return true; + } + + async setTheme( + theme: XThemePack, + whichType: XThemeTypeIdentifier = XThemeType.Light, + notifyChanges: boolean = true, + restart: boolean = true, + element?: ElementRef + ) { + // + if (isNullOrUndefined(theme)) { + // + if (notifyChanges) { + await this.issueFailedNotification(XExceptionIDs.InvalidArgs); + } + + // + return false; + } + + // + const isExists = await this.isThemeExists(theme.name); + if (!isExists) { + // + if (notifyChanges) { + await this.issueFailedNotification(XExceptionIDs.NotFound); + } + + // + return false; + } + + // + let result = false; + switch (whichType) { + // + case XThemeType.Light: + result = await this.setThemeColors(theme.light.colors, element); + break; + + // + case XThemeType.Dark: + result = await this.setThemeColors(theme.dark.colors, element); + break; + } + + // + if (result) { + // + await this.setCurrentThemeType(whichType, false); + await this.setCurrentThemeName(theme.name, false); + } + + // + if (result && notifyChanges) { + // + this.notifyChanges.emit(); + await this.issueSuccessNotification(); + } + + // + if (result && restart) { + this.settingsService.restart(); + } + + // + return result; + } + + async cloneTheme( + themeName: string, + clonedThemeName: string, + notifyChanges: boolean = true + ) { + // + const isExists = await this.isThemeExists(themeName); + if (!isExists) { + // + if (notifyChanges) { + await this.issueFailedNotification(XExceptionIDs.NotFound); + } + + // + return false; + } + + // + const isClonedThemeNameReserved = await this.isThemeExists(clonedThemeName); + if (isClonedThemeNameReserved) { + // + if (notifyChanges) { + await this.issueFailedNotification(XExceptionIDs.Duplicate); + } + + // + return false; + } + + // + const theme = await this.getTheme(themeName); + if (!theme) { + // + if (notifyChanges) { + await this.issueFailedNotification(XExceptionIDs.NotFound); + } + + // + return false; + } + + // + theme.name = clonedThemeName; + const result = await this.saveTheme(theme, false); + + // + if (result && notifyChanges) { + this.notifyChanges.emit(); + } + + // + return result; + } + + async exportTheme(themeName: string) { + // + if (isNullOrEmptyString(themeName)) { + return; + } + + // + const theme = await this.getTheme(themeName); + if (isNullOrUndefined(theme)) { + return; + } + + // + const theJSON = JSON.stringify(theme); + + // + downloadFile(theJSON, 'text/json', `x-framework-${theme.name}-theme.json`); + } + + async removeTheme(themeName: string, notifyChanges: boolean = true) { + // + const isExists = await this.isThemeExists(themeName); + if (!isExists) { + // + if (notifyChanges) { + await this.issueFailedNotification(XExceptionIDs.NotFound); + } + + // + return false; + } + + // + const currentThemeName = await this.getCurrentThemeName(); + if (!currentThemeName) { + // + if (notifyChanges) { + await this.issueFailedNotification(XExceptionIDs.NotFound); + } + + // + return false; + } + + // + const themes = await this.getThemes(); + if (!hasChild(themes) || themes.length === 1) { + // + if (notifyChanges) { + await this.issueFailedNotification(XExceptionIDs.NotFound); + } + + // + return false; + } + + // + const filteredThemes = themes.filter( + (th) => toNormalString(th.name) !== toNormalString(themeName) + ); + + // + let result = false; + try { + // + result = true; + this.writeThemes(filteredThemes); + } catch { + result = false; + } + + // + const isCurrentRemoved = + toNormalString(currentThemeName) === toNormalString(themeName); + if (isCurrentRemoved) { + // + const newCurrentTheme = filteredThemes[0]; + const themeType = await this.getCurrentThemeType(); + result = await this.setTheme(newCurrentTheme, themeType, false, false); + } + + // + if (result && notifyChanges) { + // + this.notifyChanges.emit(); + await this.issueSuccessNotification(); + } + + // + if (result && isCurrentRemoved) { + this.settingsService.restart(); + } + + // + return result; + } + + async saveTheme(theme: XThemePack, notifyChanges: boolean = true) { + // + if (isNullOrUndefined(theme)) { + // + if (notifyChanges) { + await this.issueFailedNotification(XExceptionIDs.InvalidArgs); + } + + // + return false; + } + + // + const isExists = await this.isThemeExists(theme.name); + if (isExists) { + // + if (notifyChanges) { + await this.issueFailedNotification(XExceptionIDs.Duplicate); + } + + // + return false; + } + + // + const themeType = await this.getCurrentThemeType(); + + // + const themes = await this.getThemes(); + if (!hasChild(themes)) { + // + const newThemes = [theme]; + + // + this.writeThemes(newThemes); + + // + const result = await this.setTheme(theme, themeType, true); + return result; + } else { + // + const themeIndex = themes.findIndex((th) => th.name === theme.name); + if (themeIndex === -1) { + themes.push(theme); + } else { + themes[themeIndex] = theme; + } + + // + this.writeThemes(themes); + + // + if (notifyChanges) { + // + this.notifyChanges.emit(); + await this.issueSuccessNotification(); + } + + // + return true; + } + } + + async updateTheme( + themeName: string, + updatedTheme: XThemePack, + notifiChanges: boolean = true, + restartIfCurrent: boolean = true + ) { + // + if (isNullOrEmptyString(themeName) || isNullOrUndefined(updatedTheme)) { + // + if (notifiChanges) { + await this.issueFailedNotification(XExceptionIDs.InvalidArgs); + } + + // + return false; + } + + // + const isExists = await this.isThemeExists(themeName); + if (!isExists) { + // + if (notifiChanges) { + await this.issueFailedNotification(XExceptionIDs.NotFound); + } + + // + return false; + } + + // + const currentThemeName = await this.getCurrentThemeName(); + const isCurrent = + toNormalString(themeName) === toNormalString(currentThemeName); + + // + let result = false; + const themes = this.readThemes(); + const themeIndex = themes.findIndex( + (th) => toNormalString(th.name) === toNormalString(themeName) + ); + if (themeIndex < 0) { + // + if (notifiChanges) { + await this.issueFailedNotification(XExceptionIDs.NotFound); + } + + // + return result; + } + + // + themes[themeIndex] = updatedTheme; + try { + result = true; + this.writeThemes(themes); + } catch { + // + if (notifiChanges) { + await this.issueFailedNotification(XExceptionIDs.ActionFailed); + } + + // + return false; + } + + // + if (result && isCurrent) { + // + const mode = await this.getCurrentThemeType(); + const isSetted = await this.setTheme(updatedTheme, mode, false, false); + if (!isSetted) { + // + if (notifiChanges) { + await this.issueFailedNotification(XExceptionIDs.ActionFailed); + } + + // + return false; + } + } + + // + if (result && notifiChanges) { + await this.issueSuccessNotification(); + } + + // + if (isCurrent && restartIfCurrent) { + this.settingsService.restart(); + } + + // + return result; + } + + async getCurrentTheme() { + // + const currentThemeName = await this.getCurrentThemeName(); + const isExists = await this.isThemeExists(currentThemeName); + if (!isExists) { + return null; + } + + // + const result = await this.getTheme(currentThemeName); + return result; + } + + async getThemeNames() { + // + const themes = await this.getThemes(); + if (!hasChild(themes)) { + return []; + } + + // + return themes.map((th) => th.name); + } + + async getThemes() { + // + const result = this.readThemes(); + + // + return result; + } + + async getTheme(themeName: string) { + // + const isExists = await this.isThemeExists(themeName); + if (!isExists) { + return null; + } + + // + const themes = await this.getThemes(); + return themes.find( + (th) => toNormalString(th.name) === toNormalString(themeName) + ); + } + + async isThemeExists(themeName: string) { + // + if (isNullOrEmptyString(themeName)) { + return false; + } + + // + const themes = await this.getThemeNames(); + const result = + themes.findIndex( + (th) => toNormalString(th) === toNormalString(themeName) + ) > -1; + + // + return result; + } + + async setLightMode() { + // + const currentTheme = await this.getCurrentTheme(); + if (isNullOrUndefined(currentTheme)) { + return false; + } + + // + return await this.setTheme(currentTheme, XThemeType.Light, false, true); + } + + async setDarkMode() { + // + const currentTheme = await this.getCurrentTheme(); + if (isNullOrUndefined(currentTheme)) { + return false; + } + + // + return await this.setTheme(currentTheme, XThemeType.Dark, false, true); + } + + async toggleMode() { + // + const currentTheme = await this.getCurrentTheme(); + const currentThemeMode = await this.getCurrentThemeType(); + if (isNullOrUndefined(currentTheme)) { + return false; + } + + // + const mode: XThemeType = + currentThemeMode === XThemeType.Light + ? XThemeType.Dark + : XThemeType.Light; + + // + return await this.setTheme(currentTheme, mode, false, true); + } + //#endregion + + // + //#region Private ... + private async initializeService() { + // + const currentTheme = await this.getCurrentTheme(); + const isDefaultThemeExists = !isNullOrUndefined(currentTheme); + if (!isDefaultThemeExists) { + // + const defaultTheme: XThemePack = { + name: 'default', + light: { + type: XThemeType.Light, + colors: { + // + //#region Primary ... + // + XColorPrimary: this.getCssVar(XThemeCssVars.XColorPrimary), + XColorPrimaryContrast: this.getCssVar( + XThemeCssVars.XColorPrimaryContrast + ), + + // + XColorPrimaryShade: this.getCssVar( + XThemeCssVars.XColorPrimaryShade + ), + XColorPrimaryShadeContrast: this.getCssVar( + XThemeCssVars.XColorPrimaryShadeContrast + ), + + // + XColorPrimaryTint: this.getCssVar(XThemeCssVars.XColorPrimaryTint), + XColorPrimaryTintContrast: this.getCssVar( + XThemeCssVars.XColorPrimaryTintContrast + ), + //#endregion + + // + //#region Secondary ... + // + XColorSecondary: this.getCssVar(XThemeCssVars.XColorSecondary), + XColorSecondaryContrast: this.getCssVar( + XThemeCssVars.XColorSecondaryContrast + ), + + // + XColorSecondaryShade: this.getCssVar( + XThemeCssVars.XColorSecondaryShade + ), + XColorSecondaryShadeContrast: this.getCssVar( + XThemeCssVars.XColorSecondaryShadeContrast + ), + + // + XColorSecondaryTint: this.getCssVar( + XThemeCssVars.XColorSecondaryTint + ), + XColorSecondaryTintContrast: this.getCssVar( + XThemeCssVars.XColorSecondaryTintContrast + ), + //#endregion + + // + //#region Tertiary ... + // + XColorTertiary: this.getCssVar(XThemeCssVars.XColorTertiary), + XColorTertiaryContrast: this.getCssVar( + XThemeCssVars.XColorTertiaryContrast + ), + + // + XColorTertiaryShade: this.getCssVar( + XThemeCssVars.XColorTertiaryShade + ), + XColorTertiaryShadeContrast: this.getCssVar( + XThemeCssVars.XColorTertiaryShadeContrast + ), + + // + XColorTertiaryTint: this.getCssVar( + XThemeCssVars.XColorTertiaryTint + ), + XColorTertiaryTintContrast: this.getCssVar( + XThemeCssVars.XColorTertiaryTintContrast + ), + //#endregion + + // + //#region Success ... + // + XColorSuccess: this.getCssVar(XThemeCssVars.XColorSuccess), + XColorSuccessContrast: this.getCssVar( + XThemeCssVars.XColorSuccessContrast + ), + + // + XColorSuccessShade: this.getCssVar( + XThemeCssVars.XColorSuccessShade + ), + XColorSuccessShadeContrast: this.getCssVar( + XThemeCssVars.XColorSuccessShadeContrast + ), + + // + XColorSuccessTint: this.getCssVar(XThemeCssVars.XColorSuccessTint), + XColorSuccessTintContrast: this.getCssVar( + XThemeCssVars.XColorSuccessTintContrast + ), + //#endregion + + // + //#region Warning ... + // + XColorWarning: this.getCssVar(XThemeCssVars.XColorWarning), + XColorWarningContrast: this.getCssVar( + XThemeCssVars.XColorWarningContrast + ), + + // + XColorWarningShade: this.getCssVar( + XThemeCssVars.XColorWarningShade + ), + XColorWarningShadeContrast: this.getCssVar( + XThemeCssVars.XColorWarningShadeContrast + ), + + // + XColorWarningTint: this.getCssVar(XThemeCssVars.XColorWarningTint), + XColorWarningTintContrast: this.getCssVar( + XThemeCssVars.XColorWarningTintContrast + ), + //#endregion + + // + //#region Danger ... + // + XColorDanger: this.getCssVar(XThemeCssVars.XColorDanger), + XColorDangerContrast: this.getCssVar( + XThemeCssVars.XColorDangerContrast + ), + + // + XColorDangerShade: this.getCssVar(XThemeCssVars.XColorDangerShade), + XColorDangerShadeContrast: this.getCssVar( + XThemeCssVars.XColorDangerShadeContrast + ), + + // + XColorDangerTint: this.getCssVar(XThemeCssVars.XColorDangerTint), + XColorDangerTintContrast: this.getCssVar( + XThemeCssVars.XColorDangerTintContrast + ), + //#endregion + + // + //#region Dark ... + // + XColorDark: this.getCssVar(XThemeCssVars.XColorDark), + XColorDarkContrast: this.getCssVar( + XThemeCssVars.XColorDarkContrast + ), + + // + XColorDarkShade: this.getCssVar(XThemeCssVars.XColorDarkShade), + XColorDarkShadeContrast: this.getCssVar( + XThemeCssVars.XColorDarkShadeContrast + ), + + // + XColorDarkTint: this.getCssVar(XThemeCssVars.XColorDarkTint), + XColorDarkTintContrast: this.getCssVar( + XThemeCssVars.XColorDarkTintContrast + ), + //#endregion + + // + //#region Medium ... + // + XColorMedium: this.getCssVar(XThemeCssVars.XColorMedium), + XColorMediumContrast: this.getCssVar( + XThemeCssVars.XColorMediumContrast + ), + + // + XColorMediumShade: this.getCssVar(XThemeCssVars.XColorMediumShade), + XColorMediumShadeContrast: this.getCssVar( + XThemeCssVars.XColorMediumShadeContrast + ), + + // + XColorMediumTint: this.getCssVar(XThemeCssVars.XColorMediumTint), + XColorMediumTintContrast: this.getCssVar( + XThemeCssVars.XColorMediumTintContrast + ), + //#endregion + + // + //#region Light ... + // + XColorLight: this.getCssVar(XThemeCssVars.XColorLight), + XColorLightContrast: this.getCssVar( + XThemeCssVars.XColorLightContrast + ), + + // + XColorLightShade: this.getCssVar(XThemeCssVars.XColorLightShade), + XColorLightShadeContrast: this.getCssVar( + XThemeCssVars.XColorLightShadeContrast + ), + + // + XColorLightTint: this.getCssVar(XThemeCssVars.XColorLightTint), + XColorLightTintContrast: this.getCssVar( + XThemeCssVars.XColorLightTintContrast + ), + //#endregion + }, + }, + dark: { + type: XThemeType.Dark, + colors: { + // + //#region Primary ... + // + XColorPrimary: this.getCssVar(XThemeCssVars.XColorDark), + XColorPrimaryContrast: this.getCssVar( + XThemeCssVars.XColorDarkContrast + ), + + // + XColorPrimaryShade: this.getCssVar(XThemeCssVars.XColorDarkShade), + XColorPrimaryShadeContrast: this.getCssVar( + XThemeCssVars.XColorDarkShadeContrast + ), + + // + XColorPrimaryTint: this.getCssVar(XThemeCssVars.XColorDarkTint), + XColorPrimaryTintContrast: this.getCssVar( + XThemeCssVars.XColorDarkTintContrast + ), + //#endregion + + // + //#region Secondary ... + // + XColorSecondary: this.getCssVar(XThemeCssVars.XColorDarkTint), + XColorSecondaryContrast: this.getCssVar( + XThemeCssVars.XColorDarkTintContrast + ), + + // + XColorSecondaryShade: this.getCssVar(XThemeCssVars.XColorDarkTint), + XColorSecondaryShadeContrast: this.getCssVar( + XThemeCssVars.XColorDarkTintContrast + ), + + // + XColorSecondaryTint: this.getCssVar(XThemeCssVars.XColorDarkTint), + XColorSecondaryTintContrast: this.getCssVar( + XThemeCssVars.XColorDarkTintContrast + ), + //#endregion + + // + //#region Tertiary ... + // + XColorTertiary: this.getCssVar(XThemeCssVars.XColorTertiary), + XColorTertiaryContrast: this.getCssVar( + XThemeCssVars.XColorTertiaryContrast + ), + + // + XColorTertiaryShade: this.getCssVar( + XThemeCssVars.XColorTertiaryShade + ), + XColorTertiaryShadeContrast: this.getCssVar( + XThemeCssVars.XColorTertiaryShadeContrast + ), + + // + XColorTertiaryTint: this.getCssVar( + XThemeCssVars.XColorTertiaryTint + ), + XColorTertiaryTintContrast: this.getCssVar( + XThemeCssVars.XColorTertiaryTintContrast + ), + //#endregion + + // + //#region Success ... + // + XColorSuccess: this.getCssVar(XThemeCssVars.XColorSuccess), + XColorSuccessContrast: this.getCssVar( + XThemeCssVars.XColorSuccessContrast + ), + + // + XColorSuccessShade: this.getCssVar( + XThemeCssVars.XColorSuccessShade + ), + XColorSuccessShadeContrast: this.getCssVar( + XThemeCssVars.XColorSuccessShadeContrast + ), + + // + XColorSuccessTint: this.getCssVar(XThemeCssVars.XColorSuccessTint), + XColorSuccessTintContrast: this.getCssVar( + XThemeCssVars.XColorSuccessTintContrast + ), + //#endregion + + // + //#region Warning ... + // + XColorWarning: this.getCssVar(XThemeCssVars.XColorWarning), + XColorWarningContrast: this.getCssVar( + XThemeCssVars.XColorWarningContrast + ), + + // + XColorWarningShade: this.getCssVar( + XThemeCssVars.XColorWarningShade + ), + XColorWarningShadeContrast: this.getCssVar( + XThemeCssVars.XColorWarningShadeContrast + ), + + // + XColorWarningTint: this.getCssVar(XThemeCssVars.XColorWarningTint), + XColorWarningTintContrast: this.getCssVar( + XThemeCssVars.XColorWarningTintContrast + ), + //#endregion + + // + //#region Danger ... + // + XColorDanger: this.getCssVar(XThemeCssVars.XColorDanger), + XColorDangerContrast: this.getCssVar( + XThemeCssVars.XColorDangerContrast + ), + + // + XColorDangerShade: this.getCssVar(XThemeCssVars.XColorDangerShade), + XColorDangerShadeContrast: this.getCssVar( + XThemeCssVars.XColorDangerShadeContrast + ), + + // + XColorDangerTint: this.getCssVar(XThemeCssVars.XColorDangerTint), + XColorDangerTintContrast: this.getCssVar( + XThemeCssVars.XColorDangerTintContrast + ), + //#endregion + + // + //#region Dark ... + // + XColorDark: this.getCssVar(XThemeCssVars.XColorDark), + XColorDarkContrast: this.getCssVar( + XThemeCssVars.XColorDarkContrast + ), + + // + XColorDarkShade: this.getCssVar(XThemeCssVars.XColorDarkShade), + XColorDarkShadeContrast: this.getCssVar( + XThemeCssVars.XColorDarkShadeContrast + ), + + // + XColorDarkTint: this.getCssVar(XThemeCssVars.XColorDarkTint), + XColorDarkTintContrast: this.getCssVar( + XThemeCssVars.XColorDarkTintContrast + ), + //#endregion + + // + //#region Medium ... + // + XColorMedium: this.getCssVar(XThemeCssVars.XColorMedium), + XColorMediumContrast: this.getCssVar( + XThemeCssVars.XColorMediumContrast + ), + + // + XColorMediumShade: this.getCssVar(XThemeCssVars.XColorMediumShade), + XColorMediumShadeContrast: this.getCssVar( + XThemeCssVars.XColorMediumShadeContrast + ), + + // + XColorMediumTint: this.getCssVar(XThemeCssVars.XColorMediumTint), + XColorMediumTintContrast: this.getCssVar( + XThemeCssVars.XColorMediumTintContrast + ), + //#endregion + + // + //#region Light ... + // + XColorLight: this.getCssVar(XThemeCssVars.XColorLight), + XColorLightContrast: this.getCssVar( + XThemeCssVars.XColorLightContrast + ), + + // + XColorLightShade: this.getCssVar(XThemeCssVars.XColorLightShade), + XColorLightShadeContrast: this.getCssVar( + XThemeCssVars.XColorLightShadeContrast + ), + + // + XColorLightTint: this.getCssVar(XThemeCssVars.XColorLightTint), + XColorLightTintContrast: this.getCssVar( + XThemeCssVars.XColorLightTintContrast + ), + //#endregion + }, + }, + }; + + // + await this.saveTheme(defaultTheme, false); + } else { + // + const type = await this.getCurrentThemeType(); + await this.setTheme(currentTheme, type, false, false); + } + + // + this.notifyChanges.emit(); + } + + private async issueFailedNotification(exception?: XExceptionIdentifier) { + // + if (isNullOrUndefined(exception)) { + exception = XExceptionIDs.ActionFailed; + } + + // + await this.notificationService.presentErrorNotification({ + exception, + dissmissable: false, + }); + } + + private async issueSuccessNotification() { + // + await this.notificationService.presentDefaultSuccessNotification(); + } + + // + //#region Read and Write Storage ... + private writeThemes(themes: XThemePack[]) { + // + const mKey = XThemeManagerStorageKeys.Themes; + + // + this.storageService.write(mKey, themes, true); + } + + private readThemes() { + // + const mKey = XThemeManagerStorageKeys.Themes; + + // + const isExists = this.storageService.isExists(mKey); + if (!isExists) { + return []; + } + + // + return this.storageService.readOfType(mKey, false); + } + //#endregion + + // + //#region CssVar Manipulators ... + /** + * prepare a css var name + * + * @param key css variable name + */ + private prepareCssKey(key: XThemeCssVarsIdentifier) { + return `${this.PREFIX}${key}`; + } + + /** + * remove css key prefix ... + * + * @param key a prepared css var ... + */ + private removeCssKeyPrefix(key: XThemeCssVarsIdentifier) { + return key.replace(this.PREFIX, ''); + } + + /** + * read specific css var value of an element + * @param name css variable name + * @param element destination element + */ + private getCssVar(name: XThemeCssVarsIdentifier) { + // + return getComputedStyle(document.body).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 + */ + private setCssVar( + name: XThemeCssVarsIdentifier, + value: string, + element?: ElementRef + ) { + // + if (isNullOrEmptyString(name)) { + return; + } + + // + // Setting Variable on Element if exists ... + if (element && element.nativeElement) { + (element.nativeElement as HTMLElement).style.setProperty( + this.prepareCssKey(name), + value + ); + } + + // + // Setting Variable on root ... + document.documentElement.style.setProperty(this.prepareCssKey(name), value); + } + + private async setThemeColors(theme: XThemeColors, element?: ElementRef) { + // + if (isNullOrUndefined(theme)) { + return false; + } + + // + const colorKeys = keys(theme); + colorKeys.forEach((cKey) => { + // + const cssKey = this.ColorCssVars[cKey] as string; + const ionCssKey = cssKey.replace('x-', 'ion-'); + const cssValue = toNormalString(theme[cKey]); + + // + this.setCssVar(cssKey, cssValue, element); + this.setCssVar(ionCssKey, cssValue, element); + }); + + // + return true; + } + //#endregion + //#endregion +} diff --git a/projects/x-framework-services/src/public-api.ts b/projects/x-framework-services/src/public-api.ts new file mode 100644 index 0000000..35b311c --- /dev/null +++ b/projects/x-framework-services/src/public-api.ts @@ -0,0 +1,50 @@ +/* + * Public API Surface of x-framework-services + */ + +// +//#region Export Services ... +export * from './lib/x-dialog.service'; +export * from './lib/x-manager.service'; +export * from './lib/x-storage.service'; +export * from './lib/x-security.service'; +export * from './lib/x-settings.service'; +export * from './lib/x-identifier.service'; +export * from './lib/x-geolocation.service'; +export * from './lib/x-localization.service'; +export * from './lib/x-notification.service'; +export * from './lib/x-task-manager.service'; +export * from './lib/x-theme-manager.service'; +export * from './lib/x-secure-storage.service'; +export * from './lib/x-http-interceptor.service'; +//#endregion + +// +//#region Export Directives ... +export * from './lib/directives/x-swipable.directive'; +export * from './lib/directives/x-date-presenter.directive'; +export * from './lib/directives/x-currency-formatter.directive'; +export * from './lib/directives/x-from-input-formatter.directive'; +export * from './lib/directives/x-locale-input-formatter.directive'; +//#endregion + +// +//#region Export Pipes ... +export * from './lib/pipes/x-translate.pipe'; +//#endregion + +// +//#region Tokens ... +export * from './lib/tokens/x-injectable-tokens'; +//#endregion + +// +//#region Export Config ... +export * from './lib/config/x-framework-services.config'; +//#endregion + +// +//#region Export Modules ... +export * from './lib/modules/x-directives.module'; +export * from './lib/x-framework-services.module'; +//#endregion diff --git a/projects/x-framework-services/src/test.ts b/projects/x-framework-services/src/test.ts new file mode 100644 index 0000000..52e5516 --- /dev/null +++ b/projects/x-framework-services/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-services/tsconfig.lib.ivy.json b/projects/x-framework-services/tsconfig.lib.ivy.json new file mode 100644 index 0000000..571bd13 --- /dev/null +++ b/projects/x-framework-services/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-services/tsconfig.lib.json b/projects/x-framework-services/tsconfig.lib.json new file mode 100644 index 0000000..1407202 --- /dev/null +++ b/projects/x-framework-services/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-services/tsconfig.lib.prod.json b/projects/x-framework-services/tsconfig.lib.prod.json new file mode 100644 index 0000000..06de549 --- /dev/null +++ b/projects/x-framework-services/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-services/tsconfig.spec.json b/projects/x-framework-services/tsconfig.spec.json new file mode 100644 index 0000000..715dd0a --- /dev/null +++ b/projects/x-framework-services/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..cc286db --- /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-services": [ + "dist/x-framework-services/x-framework-services", + "dist/x-framework-services" + ] + } + }, + "angularCompilerOptions": { + "strictTemplates": true, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "enableI18nLegacyMessageIdFormat": false + } +} \ No newline at end of file