{"version":3,"file":"Utility.js","sources":["../../Framework/Utility/http.ts","../../Framework/Utility/address.ts","../../Framework/Utility/arrayUtils.ts","../../Framework/Utility/linq.ts","../../Framework/Utility/guid.ts","../../Framework/Utility/stringUtils.ts","../../Framework/Utility/localeDateFormatter.ts","../../Framework/Utility/aspDateFormat.ts","../../Framework/Utility/rockDateTime.ts","../../Framework/Utility/cancellation.ts","../../Framework/Utility/dom.ts","../../Framework/Utility/util.ts","../../Framework/Utility/browserBus.ts","../../Framework/Utility/block.ts","../../Framework/Utility/booleanUtils.ts","../../Framework/Utility/cache.ts","../../Framework/Utility/suspense.ts","../../Framework/Utility/numberUtils.ts","../../Framework/Utility/component.ts","../../Framework/Utility/dateKey.ts","../../Framework/Utility/page.ts","../../Framework/Utility/dialogs.ts","../../Framework/Utility/email.ts","../../Framework/Utility/enumUtils.ts","../../Framework/Utility/fieldTypes.ts","../../Framework/Utility/file.ts","../../Framework/Utility/form.ts","../../Framework/Utility/fullscreen.ts","../../Framework/Utility/geo.ts","../../Framework/Utility/internetCalendar.ts","../../Framework/Utility/lava.ts","../../Framework/Utility/listItemBag.ts","../../Framework/Utility/mergeField.ts","../../Framework/Utility/objectUtils.ts","../../Framework/Utility/phone.ts","../../Framework/Utility/popover.ts","../../Framework/Utility/promiseUtils.ts","../../Framework/Utility/realTime.ts","../../Framework/Utility/regexPatterns.ts","../../Framework/Utility/rockCurrency.ts","../../Framework/Utility/screenSize.ts","../../Framework/Utility/slidingDateRange.ts","../../Framework/Utility/structuredContentEditor.ts","../../Framework/Utility/tooltip.ts","../../Framework/Utility/treeItemProviders.ts","../../Framework/Utility/url.ts","../../Framework/Utility/validationRules.ts"],"sourcesContent":["// \r\n// Copyright by the Spark Development Network\r\n//\r\n// Licensed under the Rock Community License (the \"License\");\r\n// you may not use this file except in compliance with the License.\r\n// You may obtain a copy of the License at\r\n//\r\n// http://www.rockrms.com/license\r\n//\r\n// Unless required by applicable law or agreed to in writing, software\r\n// distributed under the License is distributed on an \"AS IS\" BASIS,\r\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n// See the License for the specific language governing permissions and\r\n// limitations under the License.\r\n// \r\n//\r\n\r\nimport { Guid } from \"@Obsidian/Types\";\r\nimport axios, { AxiosError, AxiosProgressEvent, AxiosResponse, GenericAbortSignal } from \"axios\";\r\nimport { ListItemBag } from \"@Obsidian/ViewModels/Utility/listItemBag\";\r\nimport { HttpBodyData, HttpMethod, HttpFunctions, HttpResult, HttpUrlParams } from \"@Obsidian/Types/Utility/http\";\r\nimport { inject, provide, getCurrentInstance, ref, type Ref } from \"vue\";\r\nimport { ICancellationToken } from \"./cancellation\";\r\n\r\n\r\n// #region HTTP Requests\r\n\r\n/**\r\n * Make an API call. This is only place Axios (or AJAX library) should be referenced to allow tools like performance metrics to provide\r\n * better insights.\r\n * @param method\r\n * @param url\r\n * @param params\r\n * @param data\r\n */\r\nasync function doApiCallRaw(method: HttpMethod, url: string, params: HttpUrlParams, data: HttpBodyData, cancellationToken?: ICancellationToken): Promise> {\r\n return await axios({\r\n method,\r\n url,\r\n params,\r\n data,\r\n signal: getSignal(cancellationToken)\r\n });\r\n}\r\n\r\nfunction getSignal(cancellationToken?: ICancellationToken): GenericAbortSignal | undefined {\r\n if (cancellationToken) {\r\n const controller = new AbortController();\r\n\r\n cancellationToken.onCancellationRequested(() => {\r\n if (controller && controller.signal && !controller.signal.aborted) {\r\n controller.abort();\r\n }\r\n });\r\n\r\n return controller.signal;\r\n }\r\n}\r\n\r\n/**\r\n * Make an API call. This is a special use function that should not\r\n * normally be used. Instead call useHttp() to get the HTTP functions that\r\n * can be used.\r\n *\r\n * @param {string} method The HTTP method, such as GET\r\n * @param {string} url The endpoint to access, such as /api/campuses/\r\n * @param {object} params Query parameter object. Will be converted to ?key1=value1&key2=value2 as part of the URL.\r\n * @param {any} data This will be the body of the request\r\n */\r\nexport async function doApiCall(method: HttpMethod, url: string, params: HttpUrlParams = undefined, data: HttpBodyData = undefined, cancellationToken?: ICancellationToken): Promise> {\r\n try {\r\n const result = await doApiCallRaw(method, url, params, data, cancellationToken);\r\n\r\n return {\r\n data: result.data as T,\r\n isError: false,\r\n isSuccess: true,\r\n statusCode: result.status,\r\n errorMessage: null\r\n } as HttpResult;\r\n }\r\n catch (e) {\r\n if (axios.isAxiosError(e)) {\r\n if (e.response?.data?.Message || e?.response?.data?.message) {\r\n return {\r\n data: null,\r\n isError: true,\r\n isSuccess: false,\r\n statusCode: e.response.status,\r\n errorMessage: e?.response?.data?.Message ?? e.response.data.message\r\n } as HttpResult;\r\n }\r\n\r\n return {\r\n data: null,\r\n isError: true,\r\n isSuccess: false,\r\n statusCode: e.response?.status ?? 0,\r\n errorMessage: null\r\n } as HttpResult;\r\n }\r\n else {\r\n return {\r\n data: null,\r\n isError: true,\r\n isSuccess: false,\r\n statusCode: 0,\r\n errorMessage: null\r\n } as HttpResult;\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Make a GET HTTP request. This is a special use function that should not\r\n * normally be used. Instead call useHttp() to get the HTTP functions that\r\n * can be used.\r\n *\r\n * @param {string} url The endpoint to access, such as /api/campuses/\r\n * @param {object} params Query parameter object. Will be converted to ?key1=value1&key2=value2 as part of the URL.\r\n */\r\nexport async function get(url: string, params: HttpUrlParams = undefined): Promise> {\r\n return await doApiCall(\"GET\", url, params, undefined);\r\n}\r\n\r\n/**\r\n * Make a POST HTTP request. This is a special use function that should not\r\n * normally be used. Instead call useHttp() to get the HTTP functions that\r\n * can be used.\r\n *\r\n * @param {string} url The endpoint to access, such as /api/campuses/\r\n * @param {object} params Query parameter object. Will be converted to ?key1=value1&key2=value2 as part of the URL.\r\n * @param {any} data This will be the body of the request\r\n */\r\nexport async function post(url: string, params: HttpUrlParams = undefined, data: HttpBodyData = undefined, cancellationToken?: ICancellationToken): Promise> {\r\n return await doApiCall(\"POST\", url, params, data, cancellationToken);\r\n}\r\n\r\nconst httpFunctionsSymbol = Symbol(\"http-functions\");\r\n\r\n/**\r\n * Provides the HTTP functions that child components will use. This is an\r\n * internal API and should not be used by third party components.\r\n *\r\n * @param functions The functions that will be made available to child components.\r\n */\r\nexport function provideHttp(functions: HttpFunctions): void {\r\n provide(httpFunctionsSymbol, functions);\r\n}\r\n\r\n/**\r\n * Gets the HTTP functions that can be used by the component. This is the\r\n * standard way to make HTTP requests.\r\n *\r\n * @returns An object that contains the functions which can be called.\r\n */\r\nexport function useHttp(): HttpFunctions {\r\n let http: HttpFunctions | undefined;\r\n\r\n // Check if we are inside a setup instance. This prevents warnings\r\n // from being displayed if being called outside a setup() function.\r\n if (getCurrentInstance()) {\r\n http = inject(httpFunctionsSymbol);\r\n }\r\n\r\n return http || {\r\n doApiCall: doApiCall,\r\n get: get,\r\n post: post\r\n };\r\n}\r\n\r\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\r\ntype ApiCallerOptions = {\r\n url: string;\r\n params?: HttpUrlParams | ((...args: Args) => HttpUrlParams);\r\n data?: HttpBodyData | ((...args: Args) => HttpBodyData);\r\n onComplete?: ((data: ReturnType, ...args: Args) => void) | null | undefined;\r\n method?: \"get\" | \"post\" | \"GET\" | \"POST\" | undefined;\r\n};\r\n\r\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\r\ntype ApiCallerReturnType = {\r\n run: (...args: Args) => Promise;\r\n readonly isLoading: Ref;\r\n readonly hasError: Ref;\r\n readonly errorMessage: Ref;\r\n};\r\n\r\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\r\nexport function createApiCaller (options: ApiCallerOptions): ApiCallerReturnType {\r\n const fetchFunction = useHttp()[(options.method || \"post\").toLowerCase()];\r\n const isLoading = ref(false);\r\n const hasError = ref(false);\r\n const errorMessage = ref();\r\n\r\n return {\r\n isLoading,\r\n hasError,\r\n errorMessage,\r\n async run (...args) {\r\n isLoading.value = true;\r\n hasError.value = false;\r\n errorMessage.value = undefined;\r\n\r\n const params = typeof options.params === \"function\" ? options.params(...args) : options.params;\r\n const data = typeof options.data === \"function\" ? options.data(...args) : options.data;\r\n\r\n try {\r\n const result = (await fetchFunction(options.url, params, data)) as HttpResult;\r\n\r\n if (result.isSuccess) {\r\n if (typeof options.onComplete === \"function\") {\r\n options.onComplete(result.data as ReturnType, ...args);\r\n }\r\n\r\n return result.data as ReturnType;\r\n }\r\n else {\r\n hasError.value = true;\r\n errorMessage.value = result.errorMessage ?? undefined;\r\n }\r\n }\r\n catch (e: unknown) {\r\n hasError.value = true;\r\n\r\n if (e instanceof Error) {\r\n errorMessage.value = e.message;\r\n }\r\n else if (typeof e === \"string\") {\r\n errorMessage.value = e;\r\n }\r\n else {\r\n errorMessage.value = \"An unknown error occurred.\";\r\n }\r\n }\r\n finally {\r\n isLoading.value = false;\r\n }\r\n }\r\n };\r\n\r\n}\r\n\r\n// #endregion\r\n\r\n// #region File Upload\r\n\r\ntype FileUploadResponse = {\r\n /* eslint-disable @typescript-eslint/naming-convention */\r\n Guid: Guid;\r\n FileName: string;\r\n /* eslint-enable */\r\n};\r\n\r\n/**\r\n * Progress reporting callback used when uploading a file into Rock.\r\n */\r\nexport type UploadProgressCallback = (progress: number, total: number, percent: number) => void;\r\n\r\n/**\r\n * Options used when uploading a file into Rock to change the default behavior.\r\n */\r\nexport type UploadOptions = {\r\n /**\r\n * The base URL to use when uploading the file, must accept the same parameters\r\n * and as the standard FileUploader.ashx handler.\r\n */\r\n baseUrl?: string;\r\n\r\n /** True if the file should be uploaded as temporary, only applies to binary files. */\r\n isTemporary?: boolean;\r\n\r\n /** A function to call to report the ongoing progress of the upload. */\r\n progress: UploadProgressCallback;\r\n\r\n /** The parent entity type identifier */\r\n parentEntityTypeId?: number;\r\n\r\n /** The parent entity identifier */\r\n parentEntityId?: number;\r\n};\r\n\r\n/**\r\n * Uploads a file in the form data into Rock. This is an internal function and\r\n * should not be exported.\r\n *\r\n * @param url The URL to use for the POST request.\r\n * @param data The form data to send in the request body.\r\n * @param progress The optional callback to use to report progress.\r\n *\r\n * @returns The response from the upload handler.\r\n */\r\nasync function uploadFile(url: string, data: FormData, progress: UploadProgressCallback | undefined): Promise {\r\n let result: AxiosResponse | undefined;\r\n try {\r\n result = await axios.post(url, data, {\r\n headers: {\r\n \"Content-Type\": \"multipart/form-data\"\r\n },\r\n onUploadProgress: (event: AxiosProgressEvent) => {\r\n if (progress && event.total !== undefined) {\r\n progress(event.loaded, event.total, Math.floor(event.loaded * 100 / event.total));\r\n }\r\n }\r\n });\r\n }\r\n catch (e) {\r\n result = (e as AxiosError).response;\r\n }\r\n\r\n if (!result) {\r\n throw new Error(\"Upload failed.\");\r\n }\r\n\r\n // Check for a \"everything went perfectly fine\" response.\r\n if (result.status === 200 && typeof result.data === \"object\") {\r\n return result.data;\r\n }\r\n\r\n if (result.status === 406) {\r\n throw new Error(\"File type is not allowed.\");\r\n }\r\n\r\n if (typeof result.data === \"string\") {\r\n throw new Error(result.data);\r\n }\r\n\r\n throw new Error(\"Upload failed.\");\r\n}\r\n\r\n/**\r\n * Uploads a file to the Rock file system, usually inside the ~/Content directory.\r\n *\r\n * @param file The file to be uploaded to the server.\r\n * @param encryptedRootFolder The encrypted root folder specified by the server,\r\n * this specifies the jail the upload operation is limited to.\r\n * @param folderPath The additional sub-folder path to use inside the root folder.\r\n * @param options The options to use when uploading the file.\r\n *\r\n * @returns A ListItemBag that contains the scrubbed filename that was uploaded.\r\n */\r\nexport async function uploadContentFile(file: File, encryptedRootFolder: string, folderPath: string, options?: UploadOptions): Promise {\r\n const url = `${options?.baseUrl ?? \"/FileUploader.ashx\"}?rootFolder=${encodeURIComponent(encryptedRootFolder)}`;\r\n const formData = new FormData();\r\n\r\n formData.append(\"file\", file);\r\n\r\n if (folderPath) {\r\n formData.append(\"folderPath\", folderPath);\r\n }\r\n\r\n const result = await uploadFile(url, formData, options?.progress);\r\n\r\n return {\r\n value: \"\",\r\n text: result.FileName\r\n };\r\n}\r\n\r\n/**\r\n * Uploads a file to an asset storage provider.\r\n *\r\n * @param file The file to be uploaded to the server.\r\n * @param folderPath The additional sub-folder path to use inside the root folder.\r\n * @param assetStorageId The ID of the asset storage provider that the file is being uploaded to\r\n * @param options The options to use when uploading the file.\r\n *\r\n * @returns A ListItemBag that contains the scrubbed filename that was uploaded.\r\n */\r\nexport async function uploadAssetProviderFile(file: File, folderPath: string, assetStorageId: string, options?: UploadOptions): Promise {\r\n const url = `${options?.baseUrl ?? \"/FileUploader.ashx\"}?rootFolder=`;\r\n const formData = new FormData();\r\n\r\n if (!assetStorageId) {\r\n throw \"Asset Storage Id and Key are required.\";\r\n }\r\n\r\n formData.append(\"file\", file);\r\n formData.append(\"StorageId\", assetStorageId);\r\n formData.append(\"Key\", folderPath);\r\n formData.append(\"IsAssetStorageProviderAsset\", \"true\");\r\n\r\n const result = await uploadFile(url, formData, options?.progress);\r\n\r\n return {\r\n value: \"\",\r\n text: result.FileName\r\n };\r\n}\r\n\r\n/**\r\n * Uploads a BinaryFile into Rock. The specific storage location is defined by\r\n * the file type.\r\n *\r\n * @param file The file to be uploaded into Rock.\r\n * @param binaryFileTypeGuid The unique identifier of the BinaryFileType to handle the upload.\r\n * @param options The options ot use when uploading the file.\r\n *\r\n * @returns A ListItemBag whose value contains the new file Guid and text specifies the filename.\r\n */\r\nexport async function uploadBinaryFile(file: File, binaryFileTypeGuid: Guid, options?: UploadOptions): Promise {\r\n let url = `${options?.baseUrl ?? \"/FileUploader.ashx\"}?isBinaryFile=True&fileTypeGuid=${binaryFileTypeGuid}`;\r\n\r\n // Assume file is temporary unless specified otherwise so that files\r\n // that don't end up getting used will get cleaned up.\r\n if (options?.isTemporary === false) {\r\n url += \"&isTemporary=False\";\r\n }\r\n else {\r\n url += \"&isTemporary=True\";\r\n }\r\n\r\n if (options?.parentEntityTypeId) {\r\n url += \"&ParentEntityTypeId=\" + options.parentEntityTypeId;\r\n }\r\n\r\n if (options?.parentEntityId) {\r\n url += \"&ParentEntityId=\" + options.parentEntityId;\r\n }\r\n\r\n const formData = new FormData();\r\n formData.append(\"file\", file);\r\n\r\n const result = await uploadFile(url, formData, options?.progress);\r\n\r\n return {\r\n value: result.Guid,\r\n text: result.FileName\r\n };\r\n}\r\n\r\n// #endregion\r\n\r\nexport default {\r\n doApiCall,\r\n post,\r\n get\r\n};\r\n","// \r\n// Copyright by the Spark Development Network\r\n//\r\n// Licensed under the Rock Community License (the \"License\");\r\n// you may not use this file except in compliance with the License.\r\n// You may obtain a copy of the License at\r\n//\r\n// http://www.rockrms.com/license\r\n//\r\n// Unless required by applicable law or agreed to in writing, software\r\n// distributed under the License is distributed on an \"AS IS\" BASIS,\r\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n// See the License for the specific language governing permissions and\r\n// limitations under the License.\r\n// \r\n//\r\n\r\nimport { HttpResult } from \"@Obsidian/Types/Utility/http\";\r\nimport { AddressControlBag } from \"@Obsidian/ViewModels/Controls/addressControlBag\";\r\nimport { AddressControlValidateAddressOptionsBag } from \"@Obsidian/ViewModels/Rest/Controls/AddressControlValidateAddressOptionsBag\";\r\nimport { AddressControlValidateAddressResultsBag } from \"@Obsidian/ViewModels/Rest/Controls/AddressControlValidateAddressResultsBag\";\r\nimport { useHttp } from \"./http\";\r\n\r\nexport function getDefaultAddressControlModel(): AddressControlBag {\r\n return {\r\n state: \"AZ\",\r\n country: \"US\"\r\n };\r\n}\r\n\r\nexport function validateAddress(address: AddressControlValidateAddressOptionsBag): Promise> {\r\n const post = useHttp().post;\r\n return post(\"/api/v2/Controls/AddressControlValidateAddress\", undefined, address);\r\n}\r\n\r\nexport function getAddressString(address: AddressControlBag): Promise> {\r\n const post = useHttp().post;\r\n return post(\"/api/v2/Controls/AddressControlGetStreetAddressString\", undefined, address);\r\n}","// \r\n// Copyright by the Spark Development Network\r\n//\r\n// Licensed under the Rock Community License (the \"License\");\r\n// you may not use this file except in compliance with the License.\r\n// You may obtain a copy of the License at\r\n//\r\n// http://www.rockrms.com/license\r\n//\r\n// Unless required by applicable law or agreed to in writing, software\r\n// distributed under the License is distributed on an \"AS IS\" BASIS,\r\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n// See the License for the specific language governing permissions and\r\n// limitations under the License.\r\n// \r\n//\r\n\r\n/**\r\n * Flatten a nested array down by the given number of levels.\r\n * Meant to be a replacement for the official Array.prototype.flat, which isn't supported by all browsers we support.\r\n * Adapted from Polyfill: https://github.com/behnammodi/polyfill/blob/master/array.polyfill.js#L591\r\n *\r\n * @param arr (potentially) nested array to be flattened\r\n * @param depth The depth level specifying how deep a nested array structure should be flattened. Defaults to 1.\r\n *\r\n * @returns A new array with the sub-array elements concatenated into it.\r\n */\r\nexport function flatten(arr: T[][], depth: number = 1): T[] {\r\n const result: T[] = [];\r\n const forEach = result.forEach;\r\n\r\n const flatDeep = function (arr, depth): void {\r\n forEach.call(arr, function (val) {\r\n if (depth > 0 && Array.isArray(val)) {\r\n flatDeep(val, depth - 1);\r\n }\r\n else {\r\n result.push(val);\r\n }\r\n });\r\n };\r\n\r\n flatDeep(arr, depth);\r\n return result;\r\n}\r\n\r\n/**\r\n * Convert a single item to an array of one item. If the value is already an\r\n * array then it is just returned as is.\r\n *\r\n * @param value The value from the parent component.\r\n *\r\n * @returns The value trimmed down to just the actual selection value.\r\n */\r\nexport function forceToArray(value: T | T[] | undefined | null, multiple: boolean): T[] {\r\n if (value === undefined || value === null) {\r\n return [];\r\n }\r\n else if (Array.isArray(value)) {\r\n if (!multiple && value.length > 1) {\r\n return [value[0]];\r\n }\r\n else {\r\n return value;\r\n }\r\n }\r\n else {\r\n return [value];\r\n }\r\n}","// \r\n// Copyright by the Spark Development Network\r\n//\r\n// Licensed under the Rock Community License (the \"License\");\r\n// you may not use this file except in compliance with the License.\r\n// You may obtain a copy of the License at\r\n//\r\n// http://www.rockrms.com/license\r\n//\r\n// Unless required by applicable law or agreed to in writing, software\r\n// distributed under the License is distributed on an \"AS IS\" BASIS,\r\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n// See the License for the specific language governing permissions and\r\n// limitations under the License.\r\n// \r\n//\r\n\r\n/**\r\n * A function that will select a value from the object.\r\n */\r\ntype ValueSelector = (value: T) => string | number | boolean | null | undefined;\r\n\r\n/**\r\n * A function that will perform testing on a value to see if it meets\r\n * a certain condition and return true or false.\r\n */\r\ntype PredicateFn = (value: T, index: number) => boolean;\r\n\r\n/**\r\n * A function that will compare two values to see which one should\r\n * be ordered first.\r\n */\r\ntype ValueComparer = (a: T, b: T) => number;\r\n\r\nconst moreThanOneElement = \"More than one element was found in collection.\";\r\n\r\nconst noElementsFound = \"No element was found in collection.\";\r\n\r\n/**\r\n * Compares the values of two objects given the selector function.\r\n *\r\n * For the purposes of a compare, null and undefined are always a lower\r\n * value - unless both values are null or undefined in which case they\r\n * are considered equal.\r\n *\r\n * @param keySelector The function that will select the value.\r\n * @param descending True if this comparison should be in descending order.\r\n */\r\nfunction valueComparer(keySelector: ValueSelector, descending: boolean): ValueComparer {\r\n return (a: T, b: T): number => {\r\n const valueA = keySelector(a);\r\n const valueB = keySelector(b);\r\n\r\n // If valueA is null or undefined then it will either be considered\r\n // lower than or equal to valueB.\r\n if (valueA === undefined || valueA === null) {\r\n // If valueB is also null or undefined then they are considered equal.\r\n if (valueB === undefined || valueB === null) {\r\n return 0;\r\n }\r\n\r\n return !descending ? -1 : 1;\r\n }\r\n\r\n // If valueB is undefined or null (but valueA is not) then it is considered\r\n // a lower value than valueA.\r\n if (valueB === undefined || valueB === null) {\r\n return !descending ? 1 : -1;\r\n }\r\n\r\n // Perform a normal comparison.\r\n if (valueA > valueB) {\r\n return !descending ? 1 : -1;\r\n }\r\n else if (valueA < valueB) {\r\n return !descending ? -1 : 1;\r\n }\r\n else {\r\n return 0;\r\n }\r\n };\r\n}\r\n\r\n\r\n/**\r\n * Provides LINQ style access to an array of elements.\r\n */\r\nexport class List {\r\n /** The elements being tracked by this list. */\r\n protected elements: T[];\r\n\r\n // #region Constructors\r\n\r\n /**\r\n * Creates a new list with the given elements.\r\n *\r\n * @param elements The elements to be made available to LINQ queries.\r\n */\r\n constructor(elements?: T[]) {\r\n if (elements === undefined) {\r\n this.elements = [];\r\n }\r\n else {\r\n // Copy the array so if the caller makes changes it won't be reflected by us.\r\n this.elements = [...elements];\r\n }\r\n }\r\n\r\n /**\r\n * Creates a new List from the elements without copying to a new array.\r\n *\r\n * @param elements The elements to initialize the list with.\r\n * @returns A new list of elements.\r\n */\r\n public static fromArrayNoCopy(elements: T[]): List {\r\n const list = new List();\r\n\r\n list.elements = elements;\r\n\r\n return list;\r\n }\r\n\r\n // #endregion\r\n\r\n /**\r\n * Returns a boolean that determines if the collection contains any elements.\r\n *\r\n * @returns true if the collection contains any elements; otherwise false.\r\n */\r\n public any(): boolean;\r\n\r\n /**\r\n * Filters the list by the predicate and then returns a boolean that determines\r\n * if the filtered collection contains any elements.\r\n *\r\n * @param predicate The predicate to filter the elements by.\r\n *\r\n * @returns true if the collection contains any elements; otherwise false.\r\n */\r\n public any(predicate: PredicateFn): boolean;\r\n\r\n /**\r\n * Filters the list by the predicate and then returns a boolean that determines\r\n * if the filtered collection contains any elements.\r\n *\r\n * @param predicate The predicate to filter the elements by.\r\n *\r\n * @returns true if the collection contains any elements; otherwise false.\r\n */\r\n public any(predicate?: PredicateFn): boolean {\r\n let elements = this.elements;\r\n\r\n if (predicate !== undefined) {\r\n elements = elements.filter(predicate);\r\n }\r\n\r\n return elements.length > 0;\r\n }\r\n\r\n /**\r\n * Returns the first element from the collection if there are any elements.\r\n * Otherwise will throw an exception.\r\n *\r\n * @returns The first element in the collection.\r\n */\r\n public first(): T;\r\n\r\n /**\r\n * Filters the list by the predicate and then returns the first element\r\n * in the collection if any remain. Otherwise throws an exception.\r\n *\r\n * @param predicate The predicate to filter the elements by.\r\n *\r\n * @returns The first element in the collection.\r\n */\r\n public first(predicate: PredicateFn): T;\r\n\r\n /**\r\n * Filters the list by the predicate and then returns the first element\r\n * in the collection if any remain. Otherwise throws an exception.\r\n *\r\n * @param predicate The predicate to filter the elements by.\r\n *\r\n * @returns The first element in the collection.\r\n */\r\n public first(predicate?: PredicateFn): T {\r\n let elements = this.elements;\r\n\r\n if (predicate !== undefined) {\r\n elements = elements.filter(predicate);\r\n }\r\n\r\n if (elements.length >= 1) {\r\n return elements[0];\r\n }\r\n else {\r\n throw noElementsFound;\r\n }\r\n }\r\n\r\n /**\r\n * Returns the first element found in the collection or undefined if the\r\n * collection contains no elements.\r\n *\r\n * @returns The first element in the collection or undefined.\r\n */\r\n public firstOrUndefined(): T | undefined;\r\n\r\n /**\r\n * Filters the list by the predicate and then returns the first element\r\n * found in the collection. If no elements remain then undefined is\r\n * returned instead.\r\n *\r\n * @param predicate The predicate to filter the elements by.\r\n *\r\n * @returns The first element in the filtered collection or undefined.\r\n */\r\n public firstOrUndefined(predicate: PredicateFn): T | undefined;\r\n\r\n /**\r\n * Filters the list by the predicate and then returns the first element\r\n * found in the collection. If no elements remain then undefined is\r\n * returned instead.\r\n *\r\n * @param predicate The predicate to filter the elements by.\r\n *\r\n * @returns The first element in the filtered collection or undefined.\r\n */\r\n public firstOrUndefined(predicate?: PredicateFn): T | undefined {\r\n let elements = this.elements;\r\n\r\n if (predicate !== undefined) {\r\n elements = elements.filter(predicate);\r\n }\r\n\r\n if (elements.length === 1) {\r\n return elements[0];\r\n }\r\n else {\r\n return undefined;\r\n }\r\n }\r\n\r\n /**\r\n * Returns the last element found in the collection or undefined if the\r\n * collection contains no elements.\r\n *\r\n * @returns The last element in the collection or undefined.\r\n */\r\n public lastOrUndefined(): T | undefined;\r\n\r\n /**\r\n * Filters the list by the predicate and then returns the last element\r\n * found in the collection. If no elements remain then undefined is\r\n * returned instead.\r\n *\r\n * @param predicate The predicate to filter the elements by.\r\n *\r\n * @returns The last element in the filtered collection or undefined.\r\n */\r\n public lastOrUndefined(predicate: PredicateFn): T | undefined;\r\n\r\n /**\r\n * Filters the list by the predicate and then returns the last element\r\n * found in the collection. If no elements remain then undefined is\r\n * returned instead.\r\n *\r\n * @param predicate The predicate to filter the elements by.\r\n *\r\n * @returns The last element in the filtered collection or undefined.\r\n */\r\n public lastOrUndefined(predicate?: PredicateFn): T | undefined {\r\n let elements = this.elements;\r\n\r\n if (predicate !== undefined) {\r\n elements = elements.filter(predicate);\r\n }\r\n\r\n if (elements.length) {\r\n return elements[elements.length - 1];\r\n }\r\n else {\r\n return undefined;\r\n }\r\n }\r\n\r\n /**\r\n * Returns a single element from the collection if there is a single\r\n * element. Otherwise will throw an exception.\r\n *\r\n * @returns An element.\r\n */\r\n public single(): T;\r\n\r\n /**\r\n * Filters the list by the predicate and then returns the single remaining\r\n * element from the collection. If more than one element remains then an\r\n * exception will be thrown.\r\n *\r\n * @param predicate The predicate to filter the elements by.\r\n *\r\n * @returns An element.\r\n */\r\n public single(predicate: PredicateFn): T;\r\n\r\n /**\r\n * Filters the list by the predicate and then returns the single remaining\r\n * element from the collection. If more than one element remains then an\r\n * exception will be thrown.\r\n *\r\n * @param predicate The predicate to filter the elements by.\r\n *\r\n * @returns An element.\r\n */\r\n public single(predicate?: PredicateFn): T {\r\n let elements = this.elements;\r\n\r\n if (predicate !== undefined) {\r\n elements = elements.filter(predicate);\r\n }\r\n\r\n if (elements.length === 1) {\r\n return elements[0];\r\n }\r\n else {\r\n throw moreThanOneElement;\r\n }\r\n }\r\n\r\n /**\r\n * Returns a single element from the collection if there is a single\r\n * element. If no elements are found then undefined is returned. More\r\n * than a single element will throw an exception.\r\n *\r\n * @returns An element or undefined.\r\n */\r\n public singleOrUndefined(): T | undefined;\r\n\r\n /**\r\n * Filters the list by the predicate and then returns the single element\r\n * from the collection if there is only one remaining. If no elements\r\n * remain then undefined is returned. More than a single element will throw\r\n * an exception.\r\n *\r\n * @param predicate The predicate to filter the elements by.\r\n *\r\n * @returns An element or undefined.\r\n */\r\n public singleOrUndefined(predicate: PredicateFn): T | undefined;\r\n\r\n /**\r\n * Filters the list by the predicate and then returns the single element\r\n * from the collection if there is only one remaining. If no elements\r\n * remain then undefined is returned. More than a single element will throw\r\n * an exception.\r\n *\r\n * @param predicate The predicate to filter the elements by.\r\n *\r\n * @returns An element or undefined.\r\n */\r\n public singleOrUndefined(predicate?: PredicateFn): T | undefined {\r\n let elements = this.elements;\r\n\r\n if (predicate !== undefined) {\r\n elements = elements.filter(predicate);\r\n }\r\n\r\n if (elements.length === 0) {\r\n return undefined;\r\n }\r\n else if (elements.length === 1) {\r\n return elements[0];\r\n }\r\n else {\r\n throw moreThanOneElement;\r\n }\r\n }\r\n\r\n /**\r\n * Orders the elements of the array and returns a new list of items\r\n * in that order.\r\n *\r\n * @param keySelector The selector for the key to be ordered by.\r\n * @returns A new ordered list of elements.\r\n */\r\n public orderBy(keySelector: ValueSelector): OrderedList {\r\n const comparer = valueComparer(keySelector, false);\r\n\r\n return new OrderedList(this.elements, comparer);\r\n }\r\n\r\n /**\r\n * Orders the elements of the array in descending order and returns a\r\n * new list of items in that order.\r\n *\r\n * @param keySelector The selector for the key to be ordered by.\r\n * @returns A new ordered list of elements.\r\n */\r\n public orderByDescending(keySelector: ValueSelector): OrderedList {\r\n const comparer = valueComparer(keySelector, true);\r\n\r\n return new OrderedList(this.elements, comparer);\r\n }\r\n\r\n /**\r\n * Filters the results and returns a new list containing only the elements\r\n * that match the predicate.\r\n *\r\n * @param predicate The predicate to filter elements with.\r\n *\r\n * @returns A new collection of elements that match the predicate.\r\n */\r\n public where(predicate: PredicateFn): List {\r\n return new List(this.elements.filter(predicate));\r\n }\r\n\r\n /**\r\n * Get the elements of this list as a native array of items.\r\n *\r\n * @returns An array of items with all filters applied.\r\n */\r\n public toArray(): T[] {\r\n return [...this.elements];\r\n }\r\n}\r\n\r\n/**\r\n * A list of items that has ordering already applied.\r\n */\r\nclass OrderedList extends List {\r\n /** The base comparer to use when ordering. */\r\n private baseComparer!: ValueComparer;\r\n\r\n // #region Constructors\r\n\r\n constructor(elements: T[], baseComparer: ValueComparer) {\r\n super(elements);\r\n\r\n this.baseComparer = baseComparer;\r\n this.elements.sort(this.baseComparer);\r\n }\r\n\r\n // #endregion\r\n\r\n /**\r\n * Orders the elements of the array and returns a new list of items\r\n * in that order.\r\n *\r\n * @param keySelector The selector for the key to be ordered by.\r\n * @returns A new ordered list of elements.\r\n */\r\n public thenBy(keySelector: ValueSelector): OrderedList {\r\n const comparer = valueComparer(keySelector, false);\r\n\r\n return new OrderedList(this.elements, (a: T, b: T) => this.baseComparer(a, b) || comparer(a, b));\r\n }\r\n\r\n /**\r\n * Orders the elements of the array in descending order and returns a\r\n * new list of items in that order.\r\n *\r\n * @param keySelector The selector for the key to be ordered by.\r\n * @returns A new ordered list of elements.\r\n */\r\n public thenByDescending(keySelector: ValueSelector): OrderedList {\r\n const comparer = valueComparer(keySelector, true);\r\n\r\n return new OrderedList(this.elements, (a: T, b: T) => this.baseComparer(a, b) || comparer(a, b));\r\n }\r\n}\r\n\r\n/**\r\n * A utility class for working with iterables in a LINQ-like manner.\r\n */\r\nexport class Enumerable {\r\n protected iterableFactory: () => Iterable;\r\n\r\n /**\r\n * Creates an instance of Enumerable using a factory for the iterable.\r\n * @param iterableFactory - A factory function that produces an iterable.\r\n */\r\n constructor(iterableFactory: () => Iterable) {\r\n this.iterableFactory = iterableFactory;\r\n }\r\n\r\n /**\r\n * Creates an Enumerable from a regular iterable (e.g., Array, Set).\r\n * @param iterable - An iterable to create the Enumerable from.\r\n * @returns A new Enumerable instance.\r\n */\r\n static from(iterable: Iterable): Enumerable;\r\n\r\n /**\r\n * Creates an Enumerable from a generator function.\r\n * @param generator - A function that produces an IterableIterator.\r\n * @returns A new Enumerable instance.\r\n */\r\n static from(generator: () => IterableIterator): Enumerable;\r\n\r\n /**\r\n * Creates an Enumerable from a regular iterable (e.g., Array, Set) or a generator function.\r\n * @param source - Either an iterable or a generator function.\r\n * @returns A new Enumerable instance.\r\n */\r\n static from(source: Iterable | (() => IterableIterator)): Enumerable {\r\n if (typeof source === \"function\") {\r\n return new Enumerable(source); // Handle generator factory\r\n }\r\n else {\r\n return new Enumerable(() => source); // Handle regular iterable\r\n }\r\n }\r\n\r\n /**\r\n * Returns an iterator for the current Enumerable.\r\n * @returns An iterator for the iterable.\r\n */\r\n *[Symbol.iterator](): Iterator {\r\n // Regenerate the iterable.\r\n yield* this.iterableFactory();\r\n }\r\n\r\n /**\r\n * Filters the sequence to include only elements that satisfy the predicate.\r\n * @param predicate - A function to test each element for a condition.\r\n * @returns A new Enumerable containing the filtered elements.\r\n */\r\n where(predicate: (item: T) => boolean): Enumerable {\r\n // eslint-disable-next-line @typescript-eslint/no-this-alias\r\n const self = this;\r\n return new Enumerable(function* (): Generator {\r\n for (const item of self) {\r\n if (predicate(item)) {\r\n yield item;\r\n }\r\n }\r\n });\r\n }\r\n\r\n /**\r\n * Projects each element of the sequence into a new form.\r\n * @param selector - A function to project each element into a new form.\r\n * @returns A new Enumerable with the projected elements.\r\n */\r\n select(selector: (item: T) => U): Enumerable {\r\n // eslint-disable-next-line @typescript-eslint/no-this-alias\r\n const self = this;\r\n return new Enumerable(function* (): Generator {\r\n for (const item of self) {\r\n yield selector(item);\r\n }\r\n });\r\n }\r\n\r\n /**\r\n * Returns a new Enumerable that skips the first `count` elements of the sequence.\r\n * @param count - The number of elements to skip.\r\n * @returns A new Enumerable that skips the specified number of elements.\r\n */\r\n skip(count: number): Enumerable {\r\n // eslint-disable-next-line @typescript-eslint/no-this-alias\r\n const self = this;\r\n return new Enumerable(function* () {\r\n let skipped = 0;\r\n for (const item of self) {\r\n if (skipped++ >= count) {\r\n yield item;\r\n }\r\n }\r\n });\r\n }\r\n\r\n /**\r\n * Returns a new Enumerable that contains the first `count` elements of the sequence.\r\n * @param count - The number of elements to take.\r\n * @returns A new Enumerable containing the taken elements.\r\n */\r\n take(count: number): Enumerable {\r\n // eslint-disable-next-line @typescript-eslint/no-this-alias\r\n const self = this;\r\n return new Enumerable(function* () {\r\n let i = 0;\r\n for (const item of self) {\r\n if (i++ < count) {\r\n yield item;\r\n }\r\n else {\r\n break;\r\n }\r\n }\r\n });\r\n }\r\n\r\n /**\r\n * Returns the first element of the sequence or a default value if the sequence is empty.\r\n * @param defaultValue - The default value to return if the sequence is empty.\r\n * @returns The first element of the sequence or the default value.\r\n */\r\n firstOrDefault(defaultValue?: T): T | undefined {\r\n for (const item of this) {\r\n return item;\r\n }\r\n\r\n return defaultValue;\r\n }\r\n\r\n /**\r\n * Returns the last element of the sequence, or a default value if the sequence is empty.\r\n * @param defaultValue - The default value to return if the sequence is empty.\r\n * @returns The last element of the sequence, or the provided default value.\r\n *\r\n * @example\r\n * const numbers = Enumerable.from([1, 2, 3]);\r\n * console.log(numbers.lastOrDefault()); // Outputs: 3\r\n *\r\n * @example\r\n * const empty = Enumerable.from([]);\r\n * console.log(empty.lastOrDefault(0)); // Outputs: 0\r\n */\r\n lastOrDefault(defaultValue?: T): T | undefined {\r\n let last: T | undefined = defaultValue;\r\n\r\n for (const item of this) {\r\n // Update `last` for each element.\r\n last = item;\r\n }\r\n\r\n return last;\r\n }\r\n\r\n /**\r\n * Aggregates the elements of the sequence using a specified accumulator function and seed value.\r\n * @param accumulator - A function that accumulates each element.\r\n * @param seed - The initial value for the accumulation.\r\n * @returns The aggregated value.\r\n */\r\n aggregate(accumulator: (acc: U, item: T, index: number) => U, seed: U): U {\r\n let result = seed;\r\n let index = 0;\r\n for (const item of this) {\r\n result = accumulator(result, item, index++);\r\n }\r\n return result;\r\n }\r\n\r\n /**\r\n * Determines whether any elements in the sequence satisfy a condition or if the sequence contains any elements.\r\n * @param predicate - An optional function to test each element for a condition.\r\n * @returns `true` if any elements satisfy the condition; otherwise, `false`.\r\n */\r\n any(predicate?: (item: T) => boolean): boolean {\r\n for (const item of this) {\r\n if (!predicate || predicate(item)) {\r\n return true;\r\n }\r\n }\r\n return false;\r\n }\r\n\r\n /**\r\n * Determines whether all elements in the sequence satisfy a condition or if the sequence contains any elements.\r\n * @param predicate - An optional function to test each element for a condition.\r\n * @returns `true` if all elements satisfy the condition; otherwise, `false`.\r\n */\r\n all(predicate: (item: T) => boolean): boolean {\r\n for (const item of this) {\r\n if (!predicate(item)) {\r\n return false;\r\n }\r\n }\r\n\r\n return true;\r\n }\r\n\r\n /**\r\n * Executes a specified action for each element in the sequence.\r\n * @param action - A function to execute for each element.\r\n */\r\n forEach(action: (item: T) => void): void {\r\n for (const item of this) {\r\n action(item);\r\n }\r\n }\r\n\r\n /**\r\n * Converts the sequence into an array.\r\n * @returns An array containing all elements in the sequence.\r\n */\r\n toArray(): T[] {\r\n return Array.from(this);\r\n }\r\n\r\n /**\r\n * Converts the sequence into a List.\r\n * @returns An List containing all elements in the sequence.\r\n */\r\n toList(): List {\r\n return new List(this.toArray());\r\n }\r\n\r\n /**\r\n * Sorts the elements of the sequence in ascending order.\r\n * @param keySelector - Function to extract the key for comparison.\r\n * @param comparer - Optional comparison function for the keys.\r\n * @returns An OrderedEnumerable sorted in ascending order.\r\n */\r\n orderBy(\r\n keySelector: (item: T) => U,\r\n comparer: (a: U, b: U) => number = (a, b) => (a > b ? 1 : a < b ? -1 : 0)\r\n ): OrderedEnumerable {\r\n return new OrderedEnumerable(\r\n this.iterableFactory,\r\n (a, b) => comparer(keySelector(a), keySelector(b))\r\n );\r\n }\r\n\r\n /**\r\n * Sorts the elements of the sequence in descending order.\r\n * @param keySelector - Function to extract the key for comparison.\r\n * @param comparer - Optional comparison function for the keys.\r\n * @returns An OrderedEnumerable sorted in descending order.\r\n */\r\n orderByDescending(\r\n keySelector: (item: T) => U,\r\n comparer: (a: U, b: U) => number = (a, b) => (a > b ? 1 : a < b ? -1 : 0)\r\n ): OrderedEnumerable {\r\n const descendingComparer = (a: U, b: U): number => -comparer(a, b);\r\n return this.orderBy(keySelector, descendingComparer);\r\n }\r\n\r\n /**\r\n * Returns a generator that yields each element of the sequence paired with its index.\r\n *\r\n * @generator\r\n * @yields {[T, number]} A tuple containing the element and its zero-based index.\r\n *\r\n * @example\r\n * // Example usage with a for...of loop:\r\n * const elements = Enumerable.from(['a', 'b', 'c']);\r\n * for (const [item, index] of elements.withIndex()) {\r\n * console.log(`Index: ${index}, Item: ${item}`);\r\n * }\r\n * // Output:\r\n * // Index: 0, Item: a\r\n * // Index: 1, Item: b\r\n * // Index: 2, Item: c\r\n *\r\n * @example\r\n * // Example usage with chaining:\r\n * const indexed = Enumerable.from(['x', 'y', 'z'])\r\n * .withIndex()\r\n * .where(([item, index]) => index % 2 === 0)\r\n * .toArray();\r\n * console.log(indexed);\r\n * // Output: [['x', 0], ['z', 2]]\r\n */\r\n *withIndex(): IterableIterator<[T, number]> {\r\n let index = 0;\r\n for (const item of this) {\r\n yield [item, index++];\r\n }\r\n }\r\n\r\n /**\r\n * Filters the sequence and returns only elements of the specified type.\r\n * @template U The target type to filter by.\r\n * @param typeCheck - A runtime check function to validate the type of each element.\r\n * @returns A new Enumerable containing elements of type `U`.\r\n *\r\n * @example\r\n * const mixed: Enumerable = Enumerable.from([1, \"hello\", true, 42]);\r\n * const numbers = mixed.ofType(item => typeof item === \"number\");\r\n * console.log(numbers.toArray()); // Outputs: [1, 42]\r\n *\r\n * @example\r\n * class Animal {}\r\n * class Dog extends Animal {}\r\n * const animals: Enumerable = Enumerable.from([new Animal(), new Dog()]);\r\n * const dogs = animals.ofType(item => item instanceof Dog);\r\n * console.log(dogs.toArray()); // Outputs: [Dog instance]\r\n */\r\n ofType(typeCheck: (item: T) => item is U): Enumerable {\r\n // eslint-disable-next-line @typescript-eslint/no-this-alias\r\n const self = this;\r\n return new Enumerable(function* () {\r\n for (const item of self) {\r\n if (typeCheck(item)) {\r\n yield item;\r\n }\r\n }\r\n });\r\n }\r\n}\r\n\r\nclass OrderedEnumerable extends Enumerable {\r\n private readonly sortComparers: ((a: T, b: T) => number)[];\r\n\r\n constructor(\r\n iterableFactory: () => Iterable,\r\n initialComparer: (a: T, b: T) => number\r\n ) {\r\n super(iterableFactory);\r\n this.sortComparers = [initialComparer];\r\n }\r\n\r\n /**\r\n * Adds a secondary ascending order comparison to the current sort.\r\n * @param keySelector - Function to extract the key for comparison.\r\n * @param comparer - Optional comparison function for the keys.\r\n * @returns A new OrderedEnumerable with the additional ordering.\r\n */\r\n thenOrderBy(\r\n keySelector: (item: T) => U,\r\n comparer: (a: U, b: U) => number = (a, b) => (a > b ? 1 : a < b ? -1 : 0)\r\n ): OrderedEnumerable {\r\n // eslint-disable-next-line @typescript-eslint/no-this-alias\r\n const self = this;\r\n return new OrderedEnumerable(\r\n this.iterableFactory,\r\n (a, b) => {\r\n for (const cmp of self.sortComparers) {\r\n const result = cmp(a, b);\r\n if (result !== 0) return result;\r\n }\r\n return comparer(keySelector(a), keySelector(b));\r\n }\r\n );\r\n }\r\n\r\n /**\r\n * Adds a secondary descending order comparison to the current sort.\r\n * @param keySelector - Function to extract the key for comparison.\r\n * @param comparer - Optional comparison function for the keys.\r\n * @returns A new OrderedEnumerable with the additional ordering.\r\n */\r\n thenOrderByDescending(\r\n keySelector: (item: T) => U,\r\n comparer: (a: U, b: U) => number = (a, b) => (a > b ? 1 : a < b ? -1 : 0)\r\n ): OrderedEnumerable {\r\n const descendingComparer = (a: U, b: U): number => -comparer(a, b);\r\n return this.thenOrderBy(keySelector, descendingComparer);\r\n }\r\n\r\n override toArray(): T[] {\r\n return Array.from(this);\r\n }\r\n\r\n /**\r\n * Sorts the sequence based on the defined comparers.\r\n * @returns A new Iterable with elements sorted.\r\n */\r\n override *[Symbol.iterator](): Iterator {\r\n const array = Array.from(this.iterableFactory());\r\n array.sort((a, b) => {\r\n for (const comparer of this.sortComparers) {\r\n const result = comparer(a, b);\r\n if (result !== 0) return result;\r\n }\r\n return 0;\r\n });\r\n yield* array;\r\n }\r\n}","// \r\n// Copyright by the Spark Development Network\r\n//\r\n// Licensed under the Rock Community License (the \"License\");\r\n// you may not use this file except in compliance with the License.\r\n// You may obtain a copy of the License at\r\n//\r\n// http://www.rockrms.com/license\r\n//\r\n// Unless required by applicable law or agreed to in writing, software\r\n// distributed under the License is distributed on an \"AS IS\" BASIS,\r\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n// See the License for the specific language governing permissions and\r\n// limitations under the License.\r\n// \r\n//\r\n\r\nimport { Guid } from \"@Obsidian/Types\";\r\n\r\n/** An empty unique identifier. */\r\nexport const emptyGuid = \"00000000-0000-0000-0000-000000000000\";\r\n\r\n/**\r\n* Generates a new Guid\r\n*/\r\nexport function newGuid (): Guid {\r\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\r\n const r = Math.random() * 16 | 0;\r\n const v = c === \"x\" ? r : r & 0x3 | 0x8;\r\n return v.toString(16);\r\n });\r\n}\r\n\r\n/**\r\n * Returns a normalized Guid that can be compared with string equality (===)\r\n * @param a\r\n */\r\nexport function normalize (a: Guid | null | undefined): Guid | null {\r\n if (!a) {\r\n return null;\r\n }\r\n\r\n return a.toLowerCase();\r\n}\r\n\r\n/**\r\n * Checks if the given string is a valid Guid. To be considered valid it must\r\n * be a bare guid with hyphens. Bare means not enclosed in '{' and '}'.\r\n * \r\n * @param guid The Guid to be checked.\r\n * @returns True if the guid is valid, otherwise false.\r\n */\r\nexport function isValidGuid(guid: Guid | string): boolean {\r\n return /^[0-9A-Fa-f]{8}-(?:[0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}$/.test(guid);\r\n}\r\n\r\n/**\r\n * Converts the string value to a Guid.\r\n * \r\n * @param value The value to be converted.\r\n * @returns A Guid value or null is the string could not be parsed as a Guid.\r\n */\r\nexport function toGuidOrNull(value: string | null | undefined): Guid | null {\r\n if (value === null || value === undefined) {\r\n return null;\r\n }\r\n\r\n if (!isValidGuid(value)) {\r\n return null;\r\n }\r\n\r\n return value as Guid;\r\n}\r\n\r\n/**\r\n * Are the guids equal?\r\n * @param a\r\n * @param b\r\n */\r\nexport function areEqual (a: Guid | null | undefined, b: Guid | null | undefined): boolean {\r\n return normalize(a) === normalize(b);\r\n}\r\n\r\nexport default {\r\n newGuid,\r\n normalize,\r\n areEqual\r\n};\r\n\r\n","// \r\n// Copyright by the Spark Development Network\r\n//\r\n// Licensed under the Rock Community License (the \"License\");\r\n// you may not use this file except in compliance with the License.\r\n// You may obtain a copy of the License at\r\n//\r\n// http://www.rockrms.com/license\r\n//\r\n// Unless required by applicable law or agreed to in writing, software\r\n// distributed under the License is distributed on an \"AS IS\" BASIS,\r\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n// See the License for the specific language governing permissions and\r\n// limitations under the License.\r\n// \r\n//\r\n\r\nimport { areEqual, toGuidOrNull } from \"./guid\";\r\nimport { Pluralize } from \"@Obsidian/Libs/pluralize\";\r\n\r\n/**\r\n * Is the value an empty string?\r\n * @param val\r\n */\r\nexport function isEmpty(val: unknown): boolean {\r\n if (typeof val === \"string\") {\r\n return val.length === 0;\r\n }\r\n\r\n return false;\r\n}\r\n\r\n/**\r\n * Is the value an empty string?\r\n * @param val\r\n */\r\nexport function isWhiteSpace(val: unknown): boolean {\r\n if (typeof val === \"string\") {\r\n return val.trim().length === 0;\r\n }\r\n\r\n return false;\r\n}\r\n\r\n/**\r\n * Is the value null, undefined or whitespace?\r\n * @param val\r\n */\r\nexport function isNullOrWhiteSpace(val: unknown): boolean {\r\n return isWhiteSpace(val) || val === undefined || val === null;\r\n}\r\n\r\n/**\r\n * Turns camelCase or PascalCase strings into separate strings - \"MyCamelCaseString\" turns into \"My Camel Case String\"\r\n * @param val\r\n */\r\nexport function splitCase(val: string): string {\r\n // First, insert a space before sequences of capital letters followed by a lowercase letter (e.g., \"RESTKey\" -> \"REST Key\")\r\n val = val.replace(/([A-Z]+)([A-Z][a-z])/g, \"$1 $2\");\r\n // Then, insert a space before sequences of a lowercase letter or number followed by a capital letter (e.g., \"myKey\" -> \"my Key\")\r\n return val.replace(/([a-z0-9])([A-Z])/g, \"$1 $2\");\r\n}\r\n\r\n/**\r\n * Returns a string that has each item comma separated except for the last\r\n * which will use the word \"and\".\r\n *\r\n * @example\r\n * ['a', 'b', 'c'] => 'a, b and c'\r\n *\r\n * @param strs The strings to be joined.\r\n * @param andStr The custom string to use instead of the word \"and\".\r\n *\r\n * @returns A string that represents all the strings.\r\n */\r\nexport function asCommaAnd(strs: string[], andStr?: string): string {\r\n if (strs.length === 0) {\r\n return \"\";\r\n }\r\n\r\n if (strs.length === 1) {\r\n return strs[0];\r\n }\r\n\r\n if (!andStr) {\r\n andStr = \"and\";\r\n }\r\n\r\n if (strs.length === 2) {\r\n return `${strs[0]} ${andStr} ${strs[1]}`;\r\n }\r\n\r\n const last = strs.pop();\r\n return `${strs.join(\", \")} ${andStr} ${last}`;\r\n}\r\n\r\n/**\r\n * Convert the string to the title case.\r\n * hellO worlD => Hello World\r\n * @param str\r\n */\r\nexport function toTitleCase(str: string | null): string {\r\n if (!str) {\r\n return \"\";\r\n }\r\n\r\n return str.replace(/\\w\\S*/g, (word) => {\r\n return word.charAt(0).toUpperCase() + word.substring(1).toLowerCase();\r\n });\r\n}\r\n\r\ntype KebabCaseOptions = {\r\n /** Keep intentional multiple dashes. */\r\n preserveMultipleDashes?: boolean;\r\n /** Allow a leading dashes. */\r\n allowLeadingDashes?: boolean;\r\n /** Allow a trailing dashes. */\r\n allowTrailingDashes?: boolean;\r\n /** Keep special characters (except spaces). */\r\n allowSpecialChars?: boolean;\r\n};\r\n\r\n/**\r\n * Converts the string to kebab-case.\r\n *\r\n * @example\r\n * toKebabCase(\"helloWorLd\") // \"hello-wor-ld\"\r\n *\r\n * @param str\r\n */\r\nexport function toKebabCase(str: string, options: KebabCaseOptions = {}): string {\r\n const {\r\n preserveMultipleDashes = false,\r\n allowLeadingDashes = false,\r\n allowTrailingDashes = false,\r\n allowSpecialChars = false,\r\n } = options;\r\n\r\n let result = str.trim();\r\n\r\n // Convert camelCase or PascalCase to kebab-case.\r\n result = result.replace(/([a-z])([A-Z])/g, \"$1-$2\");\r\n\r\n // Replace spaces with dashes.\r\n result = result.replace(/\\s+/g, \"-\");\r\n\r\n // Remove special characters (if not allowed).\r\n if (!allowSpecialChars) {\r\n result = result.replace(/[^a-zA-Z0-9-]/g, \"\");\r\n }\r\n\r\n // Collapse multiple dashes unless explicitly preserved.\r\n if (!preserveMultipleDashes) {\r\n result = result.replace(/-+/g, \"-\");\r\n }\r\n\r\n // Ensure lowercase.\r\n result = result.toLowerCase();\r\n\r\n // Handle leading/trailing dashes.\r\n if (!allowLeadingDashes) {\r\n result = result.replace(/^-+/, \"\");\r\n }\r\n if (!allowTrailingDashes) {\r\n result = result.replace(/-+$/, \"\");\r\n }\r\n\r\n return result;\r\n}\r\n\r\n/**\r\n * Capitalize the first character\r\n */\r\nexport function upperCaseFirstCharacter(str: string | null): string {\r\n if (!str) {\r\n return \"\";\r\n }\r\n\r\n return str.charAt(0).toUpperCase() + str.substring(1);\r\n}\r\n\r\n/**\r\n * Pluralizes the given word. If count is specified and is equal to 1 then\r\n * the singular form of the word is returned. This will also de-pluralize a\r\n * word if required.\r\n *\r\n * @param word The word to be pluralized or singularized.\r\n * @param count An optional count to indicate when the word should be singularized.\r\n *\r\n * @returns The word in plural or singular form depending on the options.\r\n */\r\nexport function pluralize(word: string, count?: number): string {\r\n return Pluralize(word, count);\r\n}\r\n\r\n/**\r\n * Returns a singular or plural phrase depending on if the number is 1.\r\n * (0, Cat, Cats) => Cats\r\n * (1, Cat, Cats) => Cat\r\n * (2, Cat, Cats) => Cats\r\n * @param num\r\n * @param singular\r\n * @param plural\r\n */\r\nexport function pluralConditional(num: number, singular: string, plural: string): string {\r\n return num === 1 ? singular : plural;\r\n}\r\n\r\n/**\r\n * Pad the left side of a string so it is at least length characters long.\r\n *\r\n * @param str The string to be padded.\r\n * @param length The minimum length to make the string.\r\n * @param padCharacter The character to use to pad the string.\r\n */\r\nexport function padLeft(str: string | undefined | null, length: number, padCharacter: string = \" \"): string {\r\n if (padCharacter == \"\") {\r\n padCharacter = \" \";\r\n }\r\n else if (padCharacter.length > 1) {\r\n padCharacter = padCharacter.substring(0, 1);\r\n }\r\n\r\n if (!str) {\r\n return Array(length + 1).join(padCharacter);\r\n }\r\n\r\n if (str.length >= length) {\r\n return str;\r\n }\r\n\r\n return Array(length - str.length + 1).join(padCharacter) + str;\r\n}\r\n\r\n/**\r\n * Pad the right side of a string so it is at least length characters long.\r\n *\r\n * @param str The string to be padded.\r\n * @param length The minimum length to make the string.\r\n * @param padCharacter The character to use to pad the string.\r\n */\r\nexport function padRight(str: string | undefined | null, length: number, padCharacter: string = \" \"): string {\r\n if (padCharacter == \"\") {\r\n padCharacter = \" \";\r\n }\r\n else if (padCharacter.length > 1) {\r\n padCharacter = padCharacter.substring(0, 1);\r\n }\r\n\r\n if (!str) {\r\n return Array(length).join(padCharacter);\r\n }\r\n\r\n if (str.length >= length) {\r\n return str;\r\n }\r\n\r\n return str + Array(length - str.length + 1).join(padCharacter);\r\n}\r\n\r\nexport type TruncateOptions = {\r\n ellipsis?: boolean;\r\n};\r\n\r\n/**\r\n * Ensure a string does not go over the character limit. Truncation happens\r\n * on word boundaries.\r\n *\r\n * @param str The string to be truncated.\r\n * @param limit The maximum length of the resulting string.\r\n * @param options Additional options that control how truncation will happen.\r\n *\r\n * @returns The truncated string.\r\n */\r\nexport function truncate(str: string, limit: number, options?: TruncateOptions): string {\r\n // Early out if the string is already under the limit.\r\n if (str.length <= limit) {\r\n return str;\r\n }\r\n\r\n // All the whitespace characters that we can split on.\r\n const trimmable = \"\\u0009\\u000A\\u000B\\u000C\\u000D\\u0020\\u00A0\\u1680\\u180E\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200A\\u202F\\u205F\\u2028\\u2029\\u3000\\uFEFF\";\r\n const reg = new RegExp(`(?=[${trimmable}])`);\r\n const words = str.split(reg);\r\n let count = 0;\r\n\r\n // If we are appending ellipsis, then shorten the limit size.\r\n if (options && options.ellipsis === true) {\r\n limit -= 3;\r\n }\r\n\r\n // Get a list of words that will fit within our length requirements.\r\n const visibleWords = words.filter(function (word) {\r\n count += word.length;\r\n return count <= limit;\r\n });\r\n\r\n return `${visibleWords.join(\"\")}...`;\r\n}\r\n\r\n/** The regular expression that contains the characters to be escaped. */\r\nconst escapeHtmlRegExp = /[\"'&<>]/g;\r\n\r\n/** The character map of the characters to be replaced and the strings to replace them with. */\r\nconst escapeHtmlMap: Record = {\r\n '\"': \""\",\r\n \"&\": \"&\",\r\n \"'\": \"'\",\r\n \"<\": \"<\",\r\n \">\": \">\"\r\n};\r\n\r\n/**\r\n * Escapes a string so it can be used in HTML. This turns things like the <\r\n * character into the < sequence so it will still render as \"<\".\r\n *\r\n * @param str The string to be escaped.\r\n * @returns A string that has all HTML entities escaped.\r\n */\r\nexport function escapeHtml(str: string): string {\r\n return str.replace(escapeHtmlRegExp, (ch) => {\r\n return escapeHtmlMap[ch];\r\n });\r\n}\r\n\r\n/**\r\n * The default compare value function for UI controls. This checks if both values\r\n * are GUIDs and if so does a case-insensitive compare, otherwise it does a\r\n * case-sensitive compare of the two values.\r\n *\r\n * @param value The value selected in the UI.\r\n * @param itemValue The item value to be compared against.\r\n *\r\n * @returns true if the two values are considered equal; otherwise false.\r\n */\r\nexport function defaultControlCompareValue(value: string, itemValue: string): boolean {\r\n const guidValue = toGuidOrNull(value);\r\n const guidItemValue = toGuidOrNull(itemValue);\r\n\r\n if (guidValue !== null && guidItemValue !== null) {\r\n return areEqual(guidValue, guidItemValue);\r\n }\r\n\r\n return value === itemValue;\r\n}\r\n\r\n/**\r\n * Determins whether or not a given string contains any HTML tags in.\r\n *\r\n * @param value The string potentially containing HTML\r\n *\r\n * @returns true if it contains HTML, otherwise false\r\n */\r\nexport function containsHtmlTag(value: string): boolean {\r\n return /<[/0-9a-zA-Z]/.test(value);\r\n}\r\n\r\n\r\n/**\r\n * Create a 32-bit integer hash representation of a string.\r\n *\r\n * @param str The string to be hashed\r\n *\r\n * @returns The 32-bit integer hash representation of the string.\r\n */\r\nexport function createHash(str: string): number {\r\n let hash = 0;\r\n\r\n for (let i = 0; i < str.length; i++) {\r\n const chr = str.charCodeAt(i);\r\n hash = ((hash << 5) - hash) + chr;\r\n hash |= 0; // Convert to 32bit integer\r\n }\r\n\r\n return hash;\r\n}\r\n\r\n/**\r\n * Replaces all instances of `search` in `str` with `replace`.\r\n * @param str The source string.\r\n * @param search The string to search for.\r\n * @param replace The string to replace with.\r\n */\r\nexport function replaceAll(str: string, search: string, replace: string): string {\r\n return str.replace(new RegExp(search, \"g\"), replace);\r\n}\r\n\r\n/**\r\n * Attempts to parse the JSON and returns undefined if it could not be parsed.\r\n *\r\n * @param value The JSON value to parse.\r\n *\r\n * @returns The object that represents the JSON or undefined.\r\n */\r\nexport function safeParseJson(value: string | null | undefined): T | undefined {\r\n if (!value) {\r\n return undefined;\r\n }\r\n\r\n try {\r\n return JSON.parse(value);\r\n }\r\n catch {\r\n return undefined;\r\n }\r\n}\r\n\r\nexport default {\r\n asCommaAnd,\r\n containsHtmlTag,\r\n escapeHtml,\r\n splitCase,\r\n isNullOrWhiteSpace,\r\n isWhiteSpace,\r\n isEmpty,\r\n toKebabCase,\r\n toTitleCase,\r\n upperCaseFirstCharacter,\r\n pluralize,\r\n pluralConditional,\r\n padLeft,\r\n padRight,\r\n truncate,\r\n createHash,\r\n replaceAll,\r\n safeParseJson,\r\n};\r\n","// \r\n// Copyright by the Spark Development Network\r\n//\r\n// Licensed under the Rock Community License (the \"License\");\r\n// you may not use this file except in compliance with the License.\r\n// You may obtain a copy of the License at\r\n//\r\n// http://www.rockrms.com/license\r\n//\r\n// Unless required by applicable law or agreed to in writing, software\r\n// distributed under the License is distributed on an \"AS IS\" BASIS,\r\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n// See the License for the specific language governing permissions and\r\n// limitations under the License.\r\n// \r\n//\r\n\r\n/**\r\n * A helper object that provides equivalent format strings for a given locale,\r\n * for the various date libraries used throughout Rock.\r\n *\r\n * This API is internal to Rock, and is not subject to the same compatibility\r\n * standards as public APIs. It may be changed or removed without notice in any\r\n * release. You should not use this API directly in any plug-ins. Doing so can\r\n * result in application failures when updating to a new Rock release.\r\n */\r\nexport class LocaleDateFormatter {\r\n /**\r\n * The internal JavaScript date format string for the locale represented\r\n * by this formatter instance.\r\n */\r\n private jsDateFormatString: string;\r\n\r\n /**\r\n * The internal ASP C# date format string for the locale represented by this\r\n * formatter instance.\r\n */\r\n private aspDateFormatString: string | undefined;\r\n\r\n /**\r\n * The internal date picker format string for the locale represented by this\r\n * formatter instance.\r\n */\r\n private datePickerFormatString: string | undefined;\r\n\r\n /**\r\n * Creates a new instance of LocaleDateFormatter.\r\n *\r\n * @param jsDateFormatString The JavaScript date format string for the\r\n * locale represented by this formatter instance.\r\n * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format\r\n */\r\n private constructor(jsDateFormatString: string) {\r\n this.jsDateFormatString = jsDateFormatString;\r\n }\r\n\r\n /**\r\n * Creates a new instance of LocaleDateFormatter from the current locale. If\r\n * the current locale cannot be determined, a default \"en-US\" locale\r\n * formatter instance will be returned.\r\n *\r\n * @returns A LocaleDateFormatter instance representing the current locale.\r\n */\r\n public static fromCurrent(): LocaleDateFormatter {\r\n // Create an arbitrary date with recognizable numeric parts; format the\r\n // date using the current locale settings and then replace the numeric\r\n // parts with date format placeholders to get the locale date format\r\n // string. Note that month is specified as an index in the Date\r\n // constructor, so \"2\" represents month \"3\".\r\n const date = new Date(2222, 2, 4);\r\n const localeDateString = date.toLocaleDateString(undefined, {\r\n year: \"numeric\",\r\n month: \"numeric\",\r\n day: \"numeric\"\r\n });\r\n\r\n // Fall back to a default, en-US format string if any step of the\r\n // parsing fails.\r\n const defaultFormatString = \"MM/DD/YYYY\";\r\n\r\n let localeFormatString = localeDateString;\r\n\r\n // Replace the known year date part with a 2 or 4 digit format string.\r\n if (localeDateString.includes(\"2222\")) {\r\n localeFormatString = localeDateString\r\n .replace(\"2222\", \"YYYY\");\r\n }\r\n else if (localeDateString.includes(\"22\")) {\r\n localeFormatString = localeDateString\r\n .replace(\"22\", \"YY\");\r\n }\r\n else {\r\n return new LocaleDateFormatter(defaultFormatString);\r\n }\r\n\r\n // Replace the known month date part with a 1 or 2 digit format string.\r\n if (localeFormatString.includes(\"03\")) {\r\n localeFormatString = localeFormatString.replace(\"03\", \"MM\");\r\n }\r\n else if (localeFormatString.includes(\"3\")) {\r\n localeFormatString = localeFormatString.replace(\"3\", \"M\");\r\n }\r\n else {\r\n return new LocaleDateFormatter(defaultFormatString);\r\n }\r\n\r\n // Replace the known day date part with a 1 or 2 digit format string.\r\n if (localeFormatString.includes(\"04\")) {\r\n localeFormatString = localeFormatString.replace(\"04\", \"DD\");\r\n }\r\n else if (localeFormatString.includes(\"4\")) {\r\n localeFormatString = localeFormatString.replace(\"4\", \"D\");\r\n }\r\n else {\r\n return new LocaleDateFormatter(defaultFormatString);\r\n }\r\n\r\n return new LocaleDateFormatter(localeFormatString);\r\n }\r\n\r\n /**\r\n * The ASP C# date format string for the locale represented by this\r\n * formatter instance.\r\n */\r\n public get aspDateFormat(): string {\r\n if (!this.aspDateFormatString) {\r\n // Transform the standard JavaScript format string to follow C# date\r\n // formatting rules.\r\n // https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings\r\n this.aspDateFormatString = this.jsDateFormatString\r\n .replace(/D/g, \"d\")\r\n .replace(/Y/g, \"y\");\r\n }\r\n\r\n return this.aspDateFormatString;\r\n }\r\n\r\n /**\r\n * The date picker format string for the locale represented by this\r\n * formatter instance.\r\n */\r\n public get datePickerFormat(): string {\r\n if (!this.datePickerFormatString) {\r\n // Transform the standard JavaScript format string to follow the\r\n // bootstrap-datepicker library's formatting rules.\r\n // https://bootstrap-datepicker.readthedocs.io/en/stable/options.html#format\r\n this.datePickerFormatString = this.jsDateFormatString\r\n .replace(/D/g, \"d\")\r\n .replace(/M/g, \"m\")\r\n .replace(/Y/g, \"y\");\r\n }\r\n\r\n return this.datePickerFormatString;\r\n }\r\n}\r\n","// \r\n// Copyright by the Spark Development Network\r\n//\r\n// Licensed under the Rock Community License (the \"License\");\r\n// you may not use this file except in compliance with the License.\r\n// You may obtain a copy of the License at\r\n//\r\n// http://www.rockrms.com/license\r\n//\r\n// Unless required by applicable law or agreed to in writing, software\r\n// distributed under the License is distributed on an \"AS IS\" BASIS,\r\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n// See the License for the specific language governing permissions and\r\n// limitations under the License.\r\n// \r\n//\r\n\r\nimport { List } from \"./linq\";\r\nimport { padLeft, padRight } from \"./stringUtils\";\r\nimport { RockDateTime } from \"./rockDateTime\";\r\nimport { LocaleDateFormatter } from \"./localeDateFormatter\";\r\n\r\n/**\r\n * Returns a blank string if the string value is 0.\r\n *\r\n * @param value The value to check and return.\r\n * @returns The value passed in or an empty string if it equates to zero.\r\n */\r\nfunction blankIfZero(value: string): string {\r\n return parseInt(value) === 0 ? \"\" : value;\r\n}\r\n\r\n/**\r\n * Gets the 12 hour value of the given 24-hour number.\r\n *\r\n * @param hour The hour in a 24-hour format.\r\n * @returns The hour in a 12-hour format.\r\n */\r\nfunction get12HourValue(hour: number): number {\r\n if (hour == 0) {\r\n return 12;\r\n }\r\n else if (hour < 13) {\r\n return hour;\r\n }\r\n else {\r\n return hour - 12;\r\n }\r\n}\r\ntype DateFormatterCommand = (date: RockDateTime) => string;\r\n\r\nconst englishDayNames = [\"Sunday\", \"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\", \"Saturday\"];\r\nconst englishMonthNames = [\"January\", \"February\", \"March\", \"April\", \"May\", \"June\", \"July\", \"August\", \"September\", \"October\", \"November\", \"December\"];\r\n\r\nconst dateFormatters: Record = {\r\n \"yyyyy\": date => padLeft(date.year.toString(), 5, \"0\"),\r\n \"yyyy\": date => padLeft(date.year.toString(), 4, \"0\"),\r\n \"yyy\": date => padLeft(date.year.toString(), 3, \"0\"),\r\n \"yy\": date => padLeft((date.year % 100).toString(), 2, \"0\"),\r\n \"y\": date => (date.year % 100).toString(),\r\n\r\n \"MMMM\": date => englishMonthNames[date.month - 1],\r\n \"MMM\": date => englishMonthNames[date.month - 1].substr(0, 3),\r\n \"MM\": date => padLeft(date.month.toString(), 2, \"0\"),\r\n \"M\": date => date.month.toString(),\r\n\r\n \"dddd\": date => englishDayNames[date.dayOfWeek],\r\n \"ddd\": date => englishDayNames[date.dayOfWeek].substr(0, 3),\r\n \"dd\": date => padLeft(date.day.toString(), 2, \"0\"),\r\n \"d\": date => date.day.toString(),\r\n\r\n \"fffffff\": date => padRight((date.millisecond * 10000).toString(), 7, \"0\"),\r\n \"ffffff\": date => padRight((date.millisecond * 1000).toString(), 6, \"0\"),\r\n \"fffff\": date => padRight((date.millisecond * 100).toString(), 5, \"0\"),\r\n \"ffff\": date => padRight((date.millisecond * 10).toString(), 4, \"0\"),\r\n \"fff\": date => padRight(date.millisecond.toString(), 3, \"0\"),\r\n \"ff\": date => padRight(Math.floor(date.millisecond / 10).toString(), 2, \"0\"),\r\n \"f\": date => padRight(Math.floor(date.millisecond / 100).toString(), 1, \"0\"),\r\n\r\n \"FFFFFFF\": date => blankIfZero(padRight((date.millisecond * 10000).toString(), 7, \"0\")),\r\n \"FFFFFF\": date => blankIfZero(padRight((date.millisecond * 1000).toString(), 6, \"0\")),\r\n \"FFFFF\": date => blankIfZero(padRight((date.millisecond * 100).toString(), 5, \"0\")),\r\n \"FFFF\": date => blankIfZero(padRight((date.millisecond * 10).toString(), 4, \"0\")),\r\n \"FFF\": date => blankIfZero(padRight(date.millisecond.toString(), 3, \"0\")),\r\n \"FF\": date => blankIfZero(padRight(Math.floor(date.millisecond / 10).toString(), 2, \"0\")),\r\n \"F\": date => blankIfZero(padRight(Math.floor(date.millisecond / 100).toString(), 1, \"0\")),\r\n\r\n \"g\": date => date.year < 0 ? \"B.C.\" : \"A.D.\",\r\n \"gg\": date => date.year < 0 ? \"B.C.\" : \"A.D.\",\r\n\r\n \"hh\": date => padLeft(get12HourValue(date.hour).toString(), 2, \"0\"),\r\n \"h\": date => get12HourValue(date.hour).toString(),\r\n\r\n \"HH\": date => padLeft(date.hour.toString(), 2, \"0\"),\r\n \"H\": date => date.hour.toString(),\r\n\r\n \"mm\": date => padLeft(date.minute.toString(), 2, \"0\"),\r\n \"m\": date => date.minute.toString(),\r\n\r\n \"ss\": date => padLeft(date.second.toString(), 2, \"0\"),\r\n \"s\": date => date.second.toString(),\r\n\r\n \"K\": date => {\r\n const offset = date.offset;\r\n const offsetHour = Math.abs(Math.floor(offset / 60));\r\n const offsetMinute = Math.abs(offset % 60);\r\n return `${offset >= 0 ? \"+\" : \"-\"}${padLeft(offsetHour.toString(), 2, \"0\")}:${padLeft(offsetMinute.toString(), 2, \"0\")}`;\r\n },\r\n\r\n \"tt\": date => date.hour >= 12 ? \"PM\" : \"AM\",\r\n \"t\": date => date.hour >= 12 ? \"P\" : \"A\",\r\n\r\n \"zzz\": date => {\r\n const offset = date.offset;\r\n const offsetHour = Math.abs(Math.floor(offset / 60));\r\n const offsetMinute = Math.abs(offset % 60);\r\n return `${offset >= 0 ? \"+\" : \"-\"}${padLeft(offsetHour.toString(), 2, \"0\")}:${padLeft(offsetMinute.toString(), 2, \"0\")}`;\r\n },\r\n \"zz\": date => {\r\n const offset = date.offset;\r\n const offsetHour = Math.abs(Math.floor(offset / 60));\r\n return `${offset >= 0 ? \"+\" : \"-\"}${padLeft(offsetHour.toString(), 2, \"0\")}`;\r\n },\r\n \"z\": date => {\r\n const offset = date.offset;\r\n const offsetHour = Math.abs(Math.floor(offset / 60));\r\n return `${offset >= 0 ? \"+\" : \"-\"}${offsetHour}`;\r\n },\r\n\r\n \":\": () => \":\",\r\n \"/\": () => \"/\"\r\n};\r\n\r\nconst dateFormatterKeys = new List(Object.keys(dateFormatters))\r\n .orderByDescending(k => k.length)\r\n .toArray();\r\n\r\nconst currentLocaleDateFormatter = LocaleDateFormatter.fromCurrent();\r\n\r\nconst standardDateFormats: Record = {\r\n \"d\": date => formatAspDate(date, currentLocaleDateFormatter.aspDateFormat),\r\n \"D\": date => formatAspDate(date, \"dddd, MMMM dd, yyyy\"),\r\n \"t\": date => formatAspDate(date, \"h:mm tt\"),\r\n \"T\": date => formatAspDate(date, \"h:mm:ss tt\"),\r\n \"M\": date => formatAspDate(date, \"MMMM dd\"),\r\n \"m\": date => formatAspDate(date, \"MMMM dd\"),\r\n \"Y\": date => formatAspDate(date, \"yyyy MMMM\"),\r\n \"y\": date => formatAspDate(date, \"yyyy MMMM\"),\r\n \"f\": date => `${formatAspDate(date, \"D\")} ${formatAspDate(date, \"t\")}`,\r\n \"F\": date => `${formatAspDate(date, \"D\")} ${formatAspDate(date, \"T\")}`,\r\n \"g\": date => `${formatAspDate(date, \"d\")} ${formatAspDate(date, \"t\")}`,\r\n \"G\": date => `${formatAspDate(date, \"d\")} ${formatAspDate(date, \"T\")}`,\r\n \"o\": date => formatAspDate(date, `yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffffzzz`),\r\n \"O\": date => formatAspDate(date, `yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffffzzz`),\r\n \"r\": date => formatAspDate(date, `ddd, dd MMM yyyy HH':'mm':'ss 'GMT'`),\r\n \"R\": date => formatAspDate(date, `ddd, dd MMM yyyy HH':'mm':'ss 'GMT'`),\r\n \"s\": date => formatAspDate(date, `yyyy'-'MM'-'dd'T'HH':'mm':'ss`),\r\n \"u\": date => formatAspDate(date, `yyyy'-'MM'-'dd HH':'mm':'ss'Z'`),\r\n \"U\": date => {\r\n return formatAspDate(date.universalDateTime, `F`);\r\n },\r\n};\r\n\r\n/**\r\n * Formats the Date object using custom format specifiers.\r\n *\r\n * @param date The date object to be formatted.\r\n * @param format The custom format string.\r\n * @returns A string that represents the date in the specified format.\r\n */\r\nfunction formatAspCustomDate(date: RockDateTime, format: string): string {\r\n let result = \"\";\r\n\r\n for (let i = 0; i < format.length;) {\r\n let matchFound = false;\r\n\r\n for (const k of dateFormatterKeys) {\r\n if (format.substr(i, k.length) === k) {\r\n result += dateFormatters[k](date);\r\n matchFound = true;\r\n i += k.length;\r\n break;\r\n }\r\n }\r\n\r\n if (matchFound) {\r\n continue;\r\n }\r\n\r\n if (format[i] === \"\\\\\") {\r\n i++;\r\n if (i < format.length) {\r\n result += format[i++];\r\n }\r\n }\r\n else if (format[i] === \"'\") {\r\n i++;\r\n for (; i < format.length && format[i] !== \"'\"; i++) {\r\n result += format[i];\r\n }\r\n i++;\r\n }\r\n else if (format[i] === '\"') {\r\n i++;\r\n for (; i < format.length && format[i] !== '\"'; i++) {\r\n result += format[i];\r\n }\r\n i++;\r\n }\r\n else {\r\n result += format[i++];\r\n }\r\n }\r\n\r\n return result;\r\n}\r\n\r\n/**\r\n * Formats the Date object using a standard format string.\r\n *\r\n * @param date The date object to be formatted.\r\n * @param format The standard format specifier.\r\n * @returns A string that represents the date in the specified format.\r\n */\r\nfunction formatAspStandardDate(date: RockDateTime, format: string): string {\r\n if (standardDateFormats[format] !== undefined) {\r\n return standardDateFormats[format](date);\r\n }\r\n\r\n return format;\r\n}\r\n\r\n/**\r\n * Formats the given Date object using nearly the same rules as the ASP C#\r\n * format methods.\r\n *\r\n * @param date The date object to be formatted.\r\n * @param format The format string to use.\r\n */\r\nexport function formatAspDate(date: RockDateTime, format: string): string {\r\n if (format.length === 1) {\r\n return formatAspStandardDate(date, format);\r\n }\r\n else if (format.length === 2 && format[0] === \"%\") {\r\n return formatAspCustomDate(date, format[1]);\r\n }\r\n else {\r\n return formatAspCustomDate(date, format);\r\n }\r\n}\r\n","import { DateTime, FixedOffsetZone, Zone } from \"luxon\";\r\nimport { formatAspDate } from \"./aspDateFormat\";\r\nimport { DayOfWeek } from \"@Obsidian/Enums/Controls/dayOfWeek\";\r\n\r\n/**\r\n * The days of the week that are used by RockDateTime.\r\n */\r\nexport { DayOfWeek } from \"@Obsidian/Enums/Controls/dayOfWeek\";\r\n\r\n/**\r\n * The various date and time formats supported by the formatting methods.\r\n */\r\nexport const DateTimeFormat: Record = {\r\n DateFull: {\r\n year: \"numeric\",\r\n month: \"long\",\r\n day: \"numeric\"\r\n },\r\n\r\n DateMedium: {\r\n year: \"numeric\",\r\n month: \"short\",\r\n day: \"numeric\"\r\n },\r\n\r\n DateShort: {\r\n year: \"numeric\",\r\n month: \"numeric\",\r\n day: \"numeric\"\r\n },\r\n\r\n TimeShort: {\r\n hour: \"numeric\",\r\n minute: \"numeric\",\r\n },\r\n\r\n TimeWithSeconds: {\r\n hour: \"numeric\",\r\n minute: \"numeric\",\r\n second: \"numeric\"\r\n },\r\n\r\n DateTimeShort: {\r\n year: \"numeric\",\r\n month: \"numeric\",\r\n day: \"numeric\",\r\n hour: \"numeric\",\r\n minute: \"numeric\"\r\n },\r\n\r\n DateTimeShortWithSeconds: {\r\n year: \"numeric\",\r\n month: \"numeric\",\r\n day: \"numeric\",\r\n hour: \"numeric\",\r\n minute: \"numeric\",\r\n second: \"numeric\"\r\n },\r\n\r\n DateTimeMedium: {\r\n year: \"numeric\",\r\n month: \"short\",\r\n day: \"numeric\",\r\n hour: \"numeric\",\r\n minute: \"numeric\"\r\n },\r\n\r\n DateTimeMediumWithSeconds: {\r\n year: \"numeric\",\r\n month: \"short\",\r\n day: \"numeric\",\r\n hour: \"numeric\",\r\n minute: \"numeric\",\r\n second: \"numeric\"\r\n },\r\n\r\n DateTimeFull: {\r\n year: \"numeric\",\r\n month: \"long\",\r\n day: \"numeric\",\r\n hour: \"numeric\",\r\n minute: \"numeric\"\r\n },\r\n\r\n DateTimeFullWithSeconds: {\r\n year: \"numeric\",\r\n month: \"long\",\r\n day: \"numeric\",\r\n hour: \"numeric\",\r\n minute: \"numeric\",\r\n second: \"numeric\"\r\n }\r\n};\r\n\r\n/**\r\n * A date and time object that handles time zones and formatting. This class is\r\n * immutable and cannot be modified. All modifications are performed by returning\r\n * a new RockDateTime instance.\r\n */\r\nexport class RockDateTime {\r\n /** The internal DateTime object that holds our date information. */\r\n private dateTime: DateTime;\r\n\r\n // #region Constructors\r\n\r\n /**\r\n * Creates a new instance of RockDateTime.\r\n *\r\n * @param dateTime The Luxon DateTime object that is used to track the internal state.\r\n */\r\n private constructor(dateTime: DateTime) {\r\n this.dateTime = dateTime;\r\n }\r\n\r\n /**\r\n * Creates a new instance of RockDateTime from the given date and time parts.\r\n *\r\n * @param year The year of the new date.\r\n * @param month The month of the new date (1-12).\r\n * @param day The day of month of the new date.\r\n * @param hour The hour of the day.\r\n * @param minute The minute of the hour.\r\n * @param second The second of the minute.\r\n * @param millisecond The millisecond of the second.\r\n * @param zone The time zone offset to construct the date in.\r\n *\r\n * @returns A RockDateTime instance or null if the requested date was not valid.\r\n */\r\n public static fromParts(year: number, month: number, day: number, hour?: number, minute?: number, second?: number, millisecond?: number, zone?: number | string): RockDateTime | null {\r\n let luxonZone: Zone | string | undefined;\r\n\r\n if (zone !== undefined) {\r\n if (typeof zone === \"number\") {\r\n luxonZone = FixedOffsetZone.instance(zone);\r\n }\r\n else {\r\n luxonZone = zone;\r\n }\r\n }\r\n\r\n const dateTime = DateTime.fromObject({\r\n year,\r\n month,\r\n day,\r\n hour,\r\n minute,\r\n second,\r\n millisecond\r\n }, {\r\n zone: luxonZone\r\n });\r\n\r\n if (!dateTime.isValid) {\r\n return null;\r\n }\r\n\r\n return new RockDateTime(dateTime);\r\n }\r\n\r\n /**\r\n * Creates a new instance of RockDateTime that represents the time specified\r\n * as the Javascript milliseconds value. The time zone is set to the browser\r\n * time zone.\r\n *\r\n * @param milliseconds The time in milliseconds since the epoch.\r\n *\r\n * @returns A new RockDateTime instance or null if the specified date was not valid.\r\n */\r\n public static fromMilliseconds(milliseconds: number): RockDateTime | null {\r\n const dateTime = DateTime.fromMillis(milliseconds);\r\n\r\n if (!dateTime.isValid) {\r\n return null;\r\n }\r\n\r\n return new RockDateTime(dateTime);\r\n }\r\n\r\n /**\r\n * Construct a new RockDateTime instance from a Javascript Date object.\r\n *\r\n * @param date The Javascript date object that contains the date information.\r\n *\r\n * @returns A RockDateTime instance or null if the date was not valid.\r\n */\r\n public static fromJSDate(date: Date): RockDateTime | null {\r\n const dateTime = DateTime.fromJSDate(date);\r\n\r\n if (!dateTime.isValid) {\r\n return null;\r\n }\r\n\r\n return new RockDateTime(dateTime);\r\n }\r\n\r\n /**\r\n * Constructs a new RockDateTime instance by parsing the given string from\r\n * ISO 8601 format.\r\n *\r\n * @param dateString The string that contains the ISO 8601 formatted text.\r\n *\r\n * @returns A new RockDateTime instance or null if the date was not valid.\r\n */\r\n public static parseISO(dateString: string): RockDateTime | null {\r\n const dateTime = DateTime.fromISO(dateString, { setZone: true });\r\n\r\n if (!dateTime.isValid) {\r\n return null;\r\n }\r\n\r\n return new RockDateTime(dateTime);\r\n }\r\n\r\n /**\r\n * Constructs a new RockDateTime instance by parsing the given string from\r\n * RFC 1123 format. This is common in HTTP headers.\r\n *\r\n * @param dateString The string that contains the RFC 1123 formatted text.\r\n *\r\n * @returns A new RockDateTime instance or null if the date was not valid.\r\n */\r\n public static parseHTTP(dateString: string): RockDateTime | null {\r\n const dateTime = DateTime.fromHTTP(dateString, { setZone: true });\r\n\r\n if (!dateTime.isValid) {\r\n return null;\r\n }\r\n\r\n return new RockDateTime(dateTime);\r\n }\r\n\r\n /**\r\n * Creates a new RockDateTime instance that represents the current date and time.\r\n *\r\n * @returns A RockDateTime instance.\r\n */\r\n public static now(): RockDateTime {\r\n return new RockDateTime(DateTime.now());\r\n }\r\n\r\n /**\r\n * Creates a new RockDateTime instance that represents the current time in UTC.\r\n *\r\n * @returns A new RockDateTime instance in the UTC time zone.\r\n */\r\n public static utcNow(): RockDateTime {\r\n return new RockDateTime(DateTime.now().toUTC());\r\n }\r\n\r\n // #endregion\r\n\r\n // #region Properties\r\n\r\n /**\r\n * The Date portion of this RockDateTime instance. All time properties of\r\n * the returned instance will be set to 0.\r\n */\r\n public get date(): RockDateTime {\r\n const date = RockDateTime.fromParts(this.year, this.month, this.day, 0, 0, 0, 0, this.offset);\r\n\r\n if (date === null) {\r\n throw \"Could not convert to date instance.\";\r\n }\r\n\r\n return date;\r\n }\r\n\r\n /**\r\n * The raw date with no offset applied to it. Use this method when you only\r\n * care about comparing explicit dates without the time zone, as we do within\r\n * the grid's date column filter.\r\n *\r\n * This API is internal to Rock, and is not subject to the same compatibility\r\n * standards as public APIs. It may be changed or removed without notice in any\r\n * release. You should not use this API directly in any plug-ins. Doing so can\r\n * result in application failures when updating to a new Rock release.\r\n */\r\n public get rawDate(): RockDateTime {\r\n const date = RockDateTime.fromParts(this.year, this.month, this.day, 0, 0, 0, 0);\r\n\r\n if (date === null) {\r\n throw \"Could not convert to date instance.\";\r\n }\r\n\r\n return date;\r\n }\r\n\r\n /**\r\n * The day of the month represented by this instance.\r\n */\r\n public get day(): number {\r\n return this.dateTime.day;\r\n }\r\n\r\n /**\r\n * The day of the week represented by this instance.\r\n */\r\n public get dayOfWeek(): DayOfWeek {\r\n switch (this.dateTime.weekday) {\r\n case 1:\r\n return DayOfWeek.Monday;\r\n\r\n case 2:\r\n return DayOfWeek.Tuesday;\r\n\r\n case 3:\r\n return DayOfWeek.Wednesday;\r\n\r\n case 4:\r\n return DayOfWeek.Thursday;\r\n\r\n case 5:\r\n return DayOfWeek.Friday;\r\n\r\n case 6:\r\n return DayOfWeek.Saturday;\r\n\r\n case 7:\r\n return DayOfWeek.Sunday;\r\n }\r\n\r\n throw \"Could not determine day of week.\";\r\n }\r\n\r\n /**\r\n * The day of the year represented by this instance.\r\n */\r\n public get dayOfYear(): number {\r\n return this.dateTime.ordinal;\r\n }\r\n\r\n /**\r\n * The hour of the day represented by this instance.\r\n */\r\n public get hour(): number {\r\n return this.dateTime.hour;\r\n }\r\n\r\n /**\r\n * The millisecond of the second represented by this instance.\r\n */\r\n public get millisecond(): number {\r\n return this.dateTime.millisecond;\r\n }\r\n\r\n /**\r\n * The minute of the hour represented by this instance.\r\n */\r\n public get minute(): number {\r\n return this.dateTime.minute;\r\n }\r\n\r\n /**\r\n * The month of the year represented by this instance (1-12).\r\n */\r\n public get month(): number {\r\n return this.dateTime.month;\r\n }\r\n\r\n /**\r\n * The offset from UTC represented by this instance. If the timezone of this\r\n * instance is UTC-7 then the value returned is -420.\r\n */\r\n public get offset(): number {\r\n return this.dateTime.offset;\r\n }\r\n\r\n /**\r\n * The second of the minute represented by this instance.\r\n */\r\n public get second(): number {\r\n return this.dateTime.second;\r\n }\r\n\r\n /**\r\n * The year represented by this instance.\r\n */\r\n public get year(): number {\r\n return this.dateTime.year;\r\n }\r\n\r\n /**\r\n * Creates a new RockDateTime instance that represents the same point in\r\n * time represented in the local browser time zone.\r\n */\r\n public get localDateTime(): RockDateTime {\r\n return new RockDateTime(this.dateTime.toLocal());\r\n }\r\n\r\n /**\r\n * Creates a new RockDateTime instance that represents the same point in\r\n * time represented in the organization time zone.\r\n */\r\n public get organizationDateTime(): RockDateTime {\r\n throw \"Not Implemented\";\r\n }\r\n\r\n /**\r\n * Creates a new RockDateTime instance that represents the same point in\r\n * time represented in UTC.\r\n */\r\n public get universalDateTime(): RockDateTime {\r\n return new RockDateTime(this.dateTime.toUTC());\r\n }\r\n\r\n // #endregion\r\n\r\n // #region Methods\r\n\r\n /**\r\n * Creates a new RockDateTime instance that represents the date and time\r\n * after adding the number of days to this instance.\r\n *\r\n * @param days The number of days to add.\r\n *\r\n * @returns A new instance of RockDateTime that represents the new date and time.\r\n */\r\n public addDays(days: number): RockDateTime {\r\n const dateTime = this.dateTime.plus({ days: days });\r\n\r\n if (!dateTime.isValid) {\r\n throw \"Operation produced an invalid date.\";\r\n }\r\n\r\n return new RockDateTime(dateTime);\r\n }\r\n\r\n /**\r\n * Creates a new RockDateTime instance that represents the last millisecond\r\n * of the end of the month for this instance.\r\n *\r\n * @example\r\n * RockDateTime.fromJSDate(new Date(2014, 3, 3)).endOfMonth().toISOString(); //=> '2014-03-31T23:59:59.999-05:00'\r\n */\r\n public endOfMonth(): RockDateTime {\r\n const dateTime = this.dateTime.endOf(\"month\");\r\n\r\n if (!dateTime.isValid) {\r\n throw \"Operation produced an invalid date.\";\r\n }\r\n\r\n return new RockDateTime(dateTime);\r\n }\r\n\r\n /**\r\n * Creates a new RockDateTime instance that represents the date and time\r\n * after adding the number of hours to this instance.\r\n *\r\n * @param days The number of hours to add.\r\n *\r\n * @returns A new instance of RockDateTime that represents the new date and time.\r\n */\r\n public addHours(hours: number): RockDateTime {\r\n const dateTime = this.dateTime.plus({ hours: hours });\r\n\r\n if (!dateTime.isValid) {\r\n throw \"Operation produced an invalid date.\";\r\n }\r\n\r\n return new RockDateTime(dateTime);\r\n }\r\n\r\n /**\r\n * Creates a new RockDateTime instance that represents the date and time\r\n * after adding the number of milliseconds to this instance.\r\n *\r\n * @param days The number of milliseconds to add.\r\n *\r\n * @returns A new instance of RockDateTime that represents the new date and time.\r\n */\r\n public addMilliseconds(milliseconds: number): RockDateTime {\r\n const dateTime = this.dateTime.plus({ milliseconds: milliseconds });\r\n\r\n if (!dateTime.isValid) {\r\n throw \"Operation produced an invalid date.\";\r\n }\r\n\r\n return new RockDateTime(dateTime);\r\n }\r\n\r\n /**\r\n * Creates a new RockDateTime instance that represents the date and time\r\n * after adding the number of minutes to this instance.\r\n *\r\n * @param days The number of minutes to add.\r\n *\r\n * @returns A new instance of RockDateTime that represents the new date and time.\r\n */\r\n public addMinutes(minutes: number): RockDateTime {\r\n const dateTime = this.dateTime.plus({ minutes: minutes });\r\n\r\n if (!dateTime.isValid) {\r\n throw \"Operation produced an invalid date.\";\r\n }\r\n\r\n return new RockDateTime(dateTime);\r\n }\r\n\r\n /**\r\n * Creates a new RockDateTime instance that represents the date and time\r\n * after adding the number of months to this instance.\r\n *\r\n * @param days The number of months to add.\r\n *\r\n * @returns A new instance of RockDateTime that represents the new date and time.\r\n */\r\n public addMonths(months: number): RockDateTime {\r\n const dateTime = this.dateTime.plus({ months: months });\r\n\r\n if (!dateTime.isValid) {\r\n throw \"Operation produced an invalid date.\";\r\n }\r\n\r\n return new RockDateTime(dateTime);\r\n }\r\n\r\n /**\r\n * Creates a new RockDateTime instance that represents the date and time\r\n * after adding the number of seconds to this instance.\r\n *\r\n * @param days The number of seconds to add.\r\n *\r\n * @returns A new instance of RockDateTime that represents the new date and time.\r\n */\r\n public addSeconds(seconds: number): RockDateTime {\r\n const dateTime = this.dateTime.plus({ seconds: seconds });\r\n\r\n if (!dateTime.isValid) {\r\n throw \"Operation produced an invalid date.\";\r\n }\r\n\r\n return new RockDateTime(dateTime);\r\n }\r\n\r\n /**\r\n * Creates a new RockDateTime instance that represents the date and time\r\n * after adding the number of years to this instance.\r\n *\r\n * @param days The number of years to add.\r\n *\r\n * @returns A new instance of RockDateTime that represents the new date and time.\r\n */\r\n public addYears(years: number): RockDateTime {\r\n const dateTime = this.dateTime.plus({ years: years });\r\n\r\n if (!dateTime.isValid) {\r\n throw \"Operation produced an invalid date.\";\r\n }\r\n\r\n return new RockDateTime(dateTime);\r\n }\r\n\r\n /**\r\n * Converts the date time representation into the number of milliseconds\r\n * that have elapsed since the epoch (1970-01-01T00:00:00Z).\r\n *\r\n * @returns The number of milliseconds since the epoch.\r\n */\r\n public toMilliseconds(): number {\r\n return this.dateTime.toMillis();\r\n }\r\n\r\n /**\r\n * Creates a new instance of RockDateTime that represents the same point\r\n * in time as represented by the specified time zone offset.\r\n *\r\n * @param zone The time zone offset as a number or string such as \"UTC+4\".\r\n *\r\n * @returns A new RockDateTime instance that represents the specified time zone.\r\n */\r\n public toOffset(zone: number | string): RockDateTime {\r\n let dateTime: DateTime;\r\n\r\n if (typeof zone === \"number\") {\r\n dateTime = this.dateTime.setZone(FixedOffsetZone.instance(zone));\r\n }\r\n else {\r\n dateTime = this.dateTime.setZone(zone);\r\n }\r\n\r\n if (!dateTime.isValid) {\r\n throw \"Invalid time zone specified.\";\r\n }\r\n\r\n return new RockDateTime(dateTime);\r\n }\r\n\r\n /**\r\n * Formats this instance according to C# formatting rules.\r\n *\r\n * @param format The string that specifies the format to use.\r\n *\r\n * @returns A string representing this instance in the given format.\r\n */\r\n public toASPString(format: string): string {\r\n return formatAspDate(this, format);\r\n }\r\n\r\n /**\r\n * Creates a string representation of this instance in ISO8601 format.\r\n *\r\n * @returns An ISO8601 formatted string.\r\n */\r\n public toISOString(): string {\r\n // We never create an instance of RockDateTime if dateTime is not valid\r\n // and that is the only time toISO() would return null.\r\n return this.dateTime.toISO();\r\n }\r\n\r\n /**\r\n * Formats this instance using standard locale formatting rules to display\r\n * a date and time in the browsers specified locale.\r\n *\r\n * @param format The format to use when generating the string.\r\n *\r\n * @returns A string that represents the date and time in then specified format.\r\n */\r\n public toLocaleString(format: Intl.DateTimeFormatOptions): string {\r\n return this.dateTime.toLocaleString(format);\r\n }\r\n\r\n /**\r\n * Transforms the date into a human friendly elapsed time string.\r\n *\r\n * @example\r\n * // Returns \"21yrs\"\r\n * RockDateTime.fromParts(2000, 3, 4).toElapsedString();\r\n *\r\n * @returns A string that represents the amount of time that has elapsed.\r\n */\r\n public toElapsedString(currentDateTime?: RockDateTime): string {\r\n const msPerSecond = 1000;\r\n const msPerMinute = 1000 * 60;\r\n const msPerHour = 1000 * 60 * 60;\r\n const hoursPerDay = 24;\r\n const daysPerYear = 365;\r\n\r\n let start = new RockDateTime(this.dateTime);\r\n let end = currentDateTime ?? RockDateTime.now();\r\n let direction = \"Ago\";\r\n let totalMs = end.toMilliseconds() - start.toMilliseconds();\r\n\r\n if (totalMs < 0) {\r\n direction = \"From Now\";\r\n totalMs = Math.abs(totalMs);\r\n start = end;\r\n end = new RockDateTime(this.dateTime);\r\n }\r\n\r\n const totalSeconds = totalMs / msPerSecond;\r\n const totalMinutes = totalMs / msPerMinute;\r\n const totalHours = totalMs / msPerHour;\r\n const totalDays = totalHours / hoursPerDay;\r\n\r\n if (totalHours < 24) {\r\n if (totalSeconds < 2) {\r\n return `1 Second ${direction}`;\r\n }\r\n\r\n if (totalSeconds < 60) {\r\n return `${Math.floor(totalSeconds)} Seconds ${direction}`;\r\n }\r\n\r\n if (totalMinutes < 2) {\r\n return `1 Minute ${direction}`;\r\n }\r\n\r\n if (totalMinutes < 60) {\r\n return `${Math.floor(totalMinutes)} Minutes ${direction}`;\r\n }\r\n\r\n if (totalHours < 2) {\r\n return `1 Hour ${direction}`;\r\n }\r\n\r\n if (totalHours < 60) {\r\n return `${Math.floor(totalHours)} Hours ${direction}`;\r\n }\r\n }\r\n\r\n if (totalDays < 2) {\r\n return `1 Day ${direction}`;\r\n }\r\n\r\n if (totalDays < 31) {\r\n return `${Math.floor(totalDays)} Days ${direction}`;\r\n }\r\n\r\n const totalMonths = end.totalMonths(start);\r\n\r\n if (totalMonths <= 1) {\r\n return `1 Month ${direction}`;\r\n }\r\n\r\n if (totalMonths <= 18) {\r\n return `${Math.round(totalMonths)} Months ${direction}`;\r\n }\r\n\r\n const totalYears = Math.floor(totalDays / daysPerYear);\r\n\r\n if (totalYears <= 1) {\r\n return `1 Year ${direction}`;\r\n }\r\n\r\n return `${Math.round(totalYears)} Years ${direction}`;\r\n }\r\n\r\n /**\r\n * Formats this instance as a string that can be used in HTTP headers and\r\n * cookies.\r\n *\r\n * @returns A new string that conforms to RFC 1123\r\n */\r\n public toHTTPString(): string {\r\n // We never create an instance of RockDateTime if dateTime is not valid\r\n // and that is the only time toHTTP() would return null.\r\n return this.dateTime.toHTTP();\r\n }\r\n\r\n /**\r\n * Get the value of the date and time in a format that can be used in\r\n * comparisons.\r\n *\r\n * @returns A number that represents the date and time.\r\n */\r\n public valueOf(): number {\r\n return this.dateTime.valueOf();\r\n }\r\n\r\n /**\r\n * Creates a standard string representation of the date and time.\r\n *\r\n * @returns A string representation of the date and time.\r\n */\r\n public toString(): string {\r\n return this.toLocaleString(DateTimeFormat.DateTimeFull);\r\n }\r\n\r\n /**\r\n * Checks if this instance is equal to another RockDateTime instance. This\r\n * will return true if the two instances represent the same point in time,\r\n * even if they have been associated with different time zones. In other\r\n * words \"2021-09-08 12:00:00 Z\" == \"2021-09-08 14:00:00 UTC+2\".\r\n *\r\n * @param otherDateTime The other RockDateTime to be compared against.\r\n *\r\n * @returns True if the two instances represent the same point in time.\r\n */\r\n public isEqualTo(otherDateTime: RockDateTime): boolean {\r\n return this.dateTime.toMillis() === otherDateTime.dateTime.toMillis();\r\n }\r\n\r\n /**\r\n * Checks if this instance is later than another RockDateTime instance.\r\n *\r\n * @param otherDateTime The other RockDateTime to be compared against.\r\n *\r\n * @returns True if this instance represents a point in time that occurred after another point in time, regardless of time zone.\r\n */\r\n public isLaterThan(otherDateTime: RockDateTime): boolean {\r\n return this.dateTime.toMillis() > otherDateTime.dateTime.toMillis();\r\n }\r\n\r\n /**\r\n * Checks if this instance is earlier than another RockDateTime instance.\r\n *\r\n * @param otherDateTime The other RockDateTime to be compared against.\r\n *\r\n * @returns True if this instance represents a point in time that occurred before another point in time, regardless of time zone.\r\n */\r\n public isEarlierThan(otherDateTime: RockDateTime): boolean {\r\n return this.dateTime.toMillis() < otherDateTime.dateTime.toMillis();\r\n }\r\n\r\n /**\r\n * Obsolete. Use toElapsedString instead.\r\n * Calculates the elapsed time between this date and the reference date and\r\n * returns that difference in a human friendly way.\r\n *\r\n * @param otherDateTime The reference date and time. If not specified then 'now' is used.\r\n *\r\n * @returns A string that represents the elapsed time.\r\n */\r\n public humanizeElapsed(otherDateTime?: RockDateTime): string {\r\n otherDateTime = otherDateTime ?? RockDateTime.now();\r\n\r\n const totalSeconds = Math.floor((otherDateTime.dateTime.toMillis() - this.dateTime.toMillis()) / 1000);\r\n\r\n if (totalSeconds <= 1) {\r\n return \"right now\";\r\n }\r\n else if (totalSeconds < 60) { // less than 1 minute\r\n return `${totalSeconds} seconds ago`;\r\n }\r\n else if (totalSeconds >= 60 && totalSeconds <= 119) { // 1 minute ago\r\n return \"1 minute ago\";\r\n }\r\n else if (totalSeconds < 3600) { // 1 hour\r\n return `${Math.floor(totalSeconds / 60)} minutes ago`;\r\n }\r\n else if (totalSeconds < 86400) { // 1 day\r\n return `${Math.floor(totalSeconds / 3600)} hours ago`;\r\n }\r\n else if (totalSeconds < 31536000) { // 1 year\r\n return `${Math.floor(totalSeconds / 86400)} days ago`;\r\n }\r\n else {\r\n return `${Math.floor(totalSeconds / 31536000)} years ago`;\r\n }\r\n }\r\n\r\n /**\r\n * The total number of months between the two dates.\r\n * @param otherDateTime The reference date and time.\r\n * @returns An int that represents the number of months between the two dates.\r\n */\r\n public totalMonths(otherDateTime: RockDateTime): number {\r\n return ((this.year * 12) + this.month) - ((otherDateTime.year * 12) + otherDateTime.month);\r\n }\r\n\r\n // #endregion\r\n}\r\n","// \r\n// Copyright by the Spark Development Network\r\n//\r\n// Licensed under the Rock Community License (the \"License\");\r\n// you may not use this file except in compliance with the License.\r\n// You may obtain a copy of the License at\r\n//\r\n// http://www.rockrms.com/license\r\n//\r\n// Unless required by applicable law or agreed to in writing, software\r\n// distributed under the License is distributed on an \"AS IS\" BASIS,\r\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n// See the License for the specific language governing permissions and\r\n// limitations under the License.\r\n// \r\n\r\nimport mitt, { Emitter } from \"mitt\";\r\n\r\n// NOTE: Much of the logic for this was taken from VSCode's MIT licensed version:\r\n// https://github.com/microsoft/vscode/blob/342394d1e7d43d3324dc2ede1d634cffd52ba159/src/vs/base/common/cancellation.ts\r\n\r\n/**\r\n * A cancellation token can be used to instruct some operation to run but abort\r\n * if a certain condition is met.\r\n */\r\nexport interface ICancellationToken {\r\n /**\r\n * A flag signalling is cancellation has been requested.\r\n */\r\n readonly isCancellationRequested: boolean;\r\n\r\n /**\r\n * Registers a listener for when cancellation has been requested. This event\r\n * only ever fires `once` as cancellation can only happen once. Listeners\r\n * that are registered after cancellation will be called (next event loop run),\r\n * but also only once.\r\n *\r\n * @param listener The function to be called when the token has been cancelled.\r\n */\r\n onCancellationRequested(listener: () => void): void;\r\n}\r\n\r\nfunction shortcutCancelledEvent(listener: () => void): void {\r\n window.setTimeout(listener, 0);\r\n}\r\n\r\n/**\r\n * Determines if something is a cancellation token.\r\n *\r\n * @param thing The thing to be checked to see if it is a cancellation token.\r\n *\r\n * @returns true if the @{link thing} is a cancellation token, otherwise false.\r\n */\r\nexport function isCancellationToken(thing: unknown): thing is ICancellationToken {\r\n if (thing === CancellationTokenNone || thing === CancellationTokenCancelled) {\r\n return true;\r\n }\r\n if (thing instanceof MutableToken) {\r\n return true;\r\n }\r\n if (!thing || typeof thing !== \"object\") {\r\n return false;\r\n }\r\n return typeof (thing as ICancellationToken).isCancellationRequested === \"boolean\"\r\n && typeof (thing as ICancellationToken).onCancellationRequested === \"function\";\r\n}\r\n\r\n/**\r\n * A cancellation token that will never be in a cancelled state.\r\n */\r\nexport const CancellationTokenNone = Object.freeze({\r\n isCancellationRequested: false,\r\n onCancellationRequested() {\r\n // Intentionally blank.\r\n }\r\n});\r\n\r\n/**\r\n * A cancellation token that is already in a cancelled state.\r\n */\r\nexport const CancellationTokenCancelled = Object.freeze({\r\n isCancellationRequested: true,\r\n onCancellationRequested: shortcutCancelledEvent\r\n});\r\n\r\n/**\r\n * Internal implementation of a cancellation token that starts initially as\r\n * active but can later be switched to a cancelled state.\r\n */\r\nclass MutableToken implements ICancellationToken {\r\n private isCancelled: boolean = false;\r\n private emitter: Emitter> | null = null;\r\n\r\n /**\r\n * Cancels the token and fires any registered event listeners.\r\n */\r\n public cancel(): void {\r\n if (!this.isCancelled) {\r\n this.isCancelled = true;\r\n if (this.emitter) {\r\n this.emitter.emit(\"cancel\", undefined);\r\n this.emitter = null;\r\n }\r\n }\r\n }\r\n\r\n // #region ICancellationToken implementation\r\n\r\n get isCancellationRequested(): boolean {\r\n return this.isCancelled;\r\n }\r\n\r\n onCancellationRequested(listener: () => void): void {\r\n if (this.isCancelled) {\r\n return shortcutCancelledEvent(listener);\r\n }\r\n\r\n if (!this.emitter) {\r\n this.emitter = mitt();\r\n }\r\n\r\n this.emitter.on(\"cancel\", listener);\r\n }\r\n\r\n // #endregion\r\n}\r\n\r\n/**\r\n * Creates a source instance that can be used to trigger a cancellation\r\n * token into the cancelled state.\r\n */\r\nexport class CancellationTokenSource {\r\n /** The token that can be passed to functions. */\r\n private internalToken?: ICancellationToken = undefined;\r\n\r\n /**\r\n * Creates a new instance of {@link CancellationTokenSource}.\r\n *\r\n * @param parent The parent cancellation token that will also cancel this source.\r\n */\r\n constructor(parent?: ICancellationToken) {\r\n if (parent) {\r\n parent.onCancellationRequested(() => this.cancel());\r\n }\r\n }\r\n\r\n /**\r\n * The cancellation token that can be used to determine when the task\r\n * should be cancelled.\r\n */\r\n get token(): ICancellationToken {\r\n if (!this.internalToken) {\r\n // be lazy and create the token only when\r\n // actually needed\r\n this.internalToken = new MutableToken();\r\n }\r\n\r\n return this.internalToken;\r\n }\r\n\r\n /**\r\n * Moves the token into a cancelled state.\r\n */\r\n cancel(): void {\r\n if (!this.internalToken) {\r\n // Save an object creation by returning the default cancelled\r\n // token when cancellation happens before someone asks for the\r\n // token.\r\n this.internalToken = CancellationTokenCancelled;\r\n\r\n }\r\n else if (this.internalToken instanceof MutableToken) {\r\n // Actually cancel the existing token.\r\n this.internalToken.cancel();\r\n }\r\n }\r\n}\r\n","// \r\n// Copyright by the Spark Development Network\r\n//\r\n// Licensed under the Rock Community License (the \"License\");\r\n// you may not use this file except in compliance with the License.\r\n// You may obtain a copy of the License at\r\n//\r\n// http://www.rockrms.com/license\r\n//\r\n// Unless required by applicable law or agreed to in writing, software\r\n// distributed under the License is distributed on an \"AS IS\" BASIS,\r\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n// See the License for the specific language governing permissions and\r\n// limitations under the License.\r\n// \r\n//\r\n\r\nimport { Enumerable } from \"./linq\";\r\n\r\n/**\r\n * Get a unique CSS selector for any DOM element.\r\n */\r\nexport function getUniqueCssSelector(el: Element): string {\r\n const path: string[] = [];\r\n let parent: Element | null = el.parentNode as Element;\r\n\r\n while (parent) {\r\n path.unshift(`${el.tagName}:nth-child(${([] as Element[]).indexOf.call(parent.children, el) + 1})`);\r\n el = parent;\r\n parent = el.parentNode as Element;\r\n }\r\n return `${path.join(\" > \")}`.toLowerCase();\r\n}\r\n\r\nexport function* getAncestors(element: Element): IterableIterator {\r\n let parent = element.parentElement;\r\n\r\n while (parent) {\r\n yield parent;\r\n parent = parent.parentElement;\r\n }\r\n}\r\n\r\n/**\r\n * Scrolls the start of an element to the top of the window.\r\n *\r\n * This accounts for fixed elements.\r\n */\r\nexport function scrollElementStartToTop(element: Element): void {\r\n const isWithinScrolledContainer = Enumerable\r\n .from(getAncestors(element))\r\n .any(ancestorElement => {\r\n const { overflowY } = getComputedStyle(ancestorElement);\r\n const isScrollable = overflowY === \"scroll\" || overflowY === \"auto\";\r\n\r\n return isScrollable && ancestorElement.scrollHeight > ancestorElement.clientHeight;\r\n });\r\n\r\n if (isWithinScrolledContainer) {\r\n element.scrollIntoView({\r\n block: \"start\",\r\n behavior: \"smooth\"\r\n });\r\n }\r\n else {\r\n // Get the element's current position and size.\r\n const rect = element.getBoundingClientRect();\r\n\r\n const desiredLeft = rect.left < 0 ? 0 : rect.left;\r\n let desiredTop = 0;\r\n\r\n // Check if a fixed element is at the desired point.\r\n // If so, account for its size when calculating the scroll offset.\r\n const elementsAtDesiredLocation = document.elementsFromPoint(desiredLeft + 1, desiredTop + 1);\r\n const fixedElementAtDesiredLocation = elementsAtDesiredLocation.find(el => getComputedStyle(el).position === \"fixed\");\r\n\r\n if (fixedElementAtDesiredLocation) {\r\n // Adjust scroll to account for fixed element height.\r\n const { height: fixedHeight } = fixedElementAtDesiredLocation.getBoundingClientRect();\r\n desiredTop = fixedHeight;\r\n }\r\n\r\n window.scrollBy({\r\n top: rect.top - desiredTop,\r\n behavior: \"smooth\"\r\n });\r\n }\r\n}\r\n\r\n/**\r\n * Finds the direct child of `element` containing the `descendantElement`.\r\n * @param element The element to be searched.\r\n * @param descendantElement The descendant element to search for.\r\n */\r\nexport function findDirectChildContaining(element: HTMLElement, descendantElement: HTMLElement): HTMLElement | null {\r\n let current: Element | null = descendantElement;\r\n\r\n while (current && current !== element) {\r\n if (current.parentElement === element && isHTMLElement(current)) {\r\n // Found the direct child of element that contains descendantElement.\r\n return current;\r\n }\r\n\r\n // Move up the DOM tree.\r\n current = current.parentElement;\r\n }\r\n\r\n // No direct child was found.\r\n return null;\r\n}\r\n\r\n/**\r\n * Finds the nearest element to a specific coordinate in the y-direction.\r\n * @param iframe\r\n * @param x The X position (in pixels) to search.\r\n * @param y The Y position (in pixels) to search.\r\n * @param selector The CSS selector used to find the matching element.\r\n * @param xRange The horizontal distance (in pixels) left and right of the `xPosition` to search for an element.\r\n * @param yRange The vertical distance (in pixels) above and below the `yPosition` to search for an element.\r\n * @returns\r\n */\r\nexport function findNearestIFrameElementFromPoint(\r\n iframe: HTMLIFrameElement,\r\n x: number,\r\n y: number,\r\n selector: string,\r\n xRange: number = 10,\r\n yRange: number = 10\r\n): { element: HTMLElement | null, isAbove: boolean, isBelow: boolean, isLeft: boolean, isRight: boolean } {\r\n const iframeDoc = iframe.contentDocument || iframe.contentWindow?.document;\r\n if (!iframeDoc) {\r\n console.error(\"Unable to access iframe content.\");\r\n return { element: null, isAbove: false, isBelow: false, isLeft: false, isRight: false };\r\n }\r\n\r\n const iframeRect = iframe.getBoundingClientRect();\r\n\r\n let closestElement: Element | null = null;\r\n let isAbove: boolean = false;\r\n let isBelow: boolean = false;\r\n let isLeft: boolean = false;\r\n let isRight: boolean = false;\r\n let smallestDistance = Infinity;\r\n\r\n // Search within the specified xRange and yRange.\r\n for (let dx = -xRange; dx <= xRange; dx += 1) {\r\n for (let dy = -yRange; dy <= yRange; dy += 1) {\r\n Enumerable.from(iframeDoc.elementsFromPoint(x + dx, y + dy))\r\n .where(element => element.matches(selector))\r\n .ofType(isHTMLElement)\r\n .forEach(element => {\r\n const rect = element.getBoundingClientRect();\r\n\r\n // Calculate the distance from the current position to the element.\r\n const elementX = rect.left - iframeRect.left;\r\n const elementY = rect.top - iframeRect.top;\r\n const distance = Math.sqrt(\r\n Math.pow(elementX - x, 2) + Math.pow(elementY - y, 2)\r\n );\r\n\r\n // Update the closest element if this one is nearer.\r\n if (distance < smallestDistance) {\r\n smallestDistance = distance;\r\n\r\n closestElement = element;\r\n isAbove = dy < 0;\r\n isBelow = dy > 0;\r\n isLeft = dx < 0;\r\n isRight = dx > 0;\r\n }\r\n });\r\n }\r\n }\r\n\r\n return {\r\n element: closestElement,\r\n isAbove,\r\n isBelow,\r\n isLeft,\r\n isRight\r\n };\r\n}\r\n\r\n/**\r\n * Gets the window associated with an element.\r\n */\r\nexport function getWindow(el: Element): (Window & typeof globalThis) | null {\r\n return el.ownerDocument.defaultView;\r\n}\r\n\r\n/**\r\n * Determines whether the argument is an Element.\r\n */\r\nexport function isElement(el: unknown): el is Element {\r\n // This handles context mismatch when checking iframe elements.\r\n const elWindow = el?.[\"ownerDocument\"]?.[\"defaultView\"] as (Window & typeof globalThis);\r\n\r\n return el instanceof Element || (!!elWindow && (el instanceof elWindow.Element));\r\n}\r\n\r\n/**\r\n * Determines whether the argument is an HTMLElement.\r\n */\r\nexport function isHTMLElement(el: unknown): el is HTMLElement {\r\n // This handles context mismatch when checking iframe elements.\r\n const elWindow = el?.[\"ownerDocument\"]?.[\"defaultView\"] as (Window & typeof globalThis);\r\n\r\n return el instanceof HTMLElement || (!!elWindow && (el instanceof elWindow.HTMLElement));\r\n}\r\n\r\n/**\r\n * Determines whether the argument is an HTMLAnchorElement.\r\n */\r\nexport function isHTMLAnchorElement(el: unknown): el is HTMLAnchorElement {\r\n // This handles context mismatch when checking iframe elements.\r\n const elWindow = el?.[\"ownerDocument\"]?.[\"defaultView\"] as (Window & typeof globalThis);\r\n\r\n return !!elWindow && el instanceof elWindow.HTMLAnchorElement;\r\n}\r\n\r\n/**\r\n * Determines whether the argument is a Document.\r\n */\r\nexport function isDocument(doc: unknown): doc is Document {\r\n // This handles context mismatch when checking iframe elements.\r\n const docWindow = doc?.[\"defaultView\"] as (Window & typeof globalThis);\r\n\r\n return !!docWindow && doc instanceof docWindow.Document;\r\n}\r\n\r\n/**\r\n * Determines whether the argument is a MouseEvent.\r\n * @param event\r\n * @returns\r\n */\r\nexport function isMouseEvent(event: unknown): event is MouseEvent {\r\n // This handles context mismatch when checking iframe elements.\r\n const eventWindow = event?.[\"view\"] as (Window & typeof globalThis);\r\n\r\n return !!eventWindow && event instanceof eventWindow.MouseEvent;\r\n}\r\n\r\n/**\r\n * Determines whether the argument is a TouchEvent.\r\n * @param event\r\n * @returns\r\n */\r\nexport function isTouchEvent(event: unknown): event is TouchEvent {\r\n // This handles context mismatch when checking iframe elements.\r\n const eventWindow = event?.[\"view\"] as (Window & typeof globalThis);\r\n\r\n // TouchEvent may not be supported in some browsers.\r\n return !!eventWindow?.TouchEvent && event instanceof eventWindow.TouchEvent;\r\n}\r\n\r\n/**\r\n * Removes white space content from an element.\r\n *\r\n * This is important for applying :empty styles.\r\n */\r\nexport function removeWhiteSpaceFromElement(element: Element, selector?: string): void {\r\n if ((!selector || element.matches(selector))\r\n && element.childNodes.length\r\n && Enumerable.from(element.childNodes).all(n => n.nodeType === Node.TEXT_NODE)\r\n && !element.textContent?.trim()\r\n ) {\r\n element.innerHTML = \"\";\r\n }\r\n}\r\n\r\n/**\r\n * Removes white space content from an element and its children.\r\n *\r\n * This is important for applying :empty styles.\r\n */\r\nexport function removeWhiteSpaceFromElementAndChildElements(element: Element, selector?: string): void {\r\n removeWhiteSpaceFromChildElements(element, selector);\r\n removeWhiteSpaceFromElement(element, selector);\r\n}\r\n\r\n/**\r\n * Removes white space content from an element's children.\r\n *\r\n * This is important for applying :empty styles.\r\n */\r\nexport function removeWhiteSpaceFromChildElements(element: Document | Element, selector?: string): void {\r\n // Clear white space from elements matching the selector so :empty styles gets applied.\r\n if (selector) {\r\n element.querySelectorAll(selector)\r\n .forEach(el => {\r\n removeWhiteSpaceFromElement(el, selector);\r\n });\r\n }\r\n else {\r\n Enumerable.from(element.children)\r\n .forEach(el => {\r\n removeWhiteSpaceFromElement(el, selector);\r\n });\r\n }\r\n}","// \r\n// Copyright by the Spark Development Network\r\n//\r\n// Licensed under the Rock Community License (the \"License\");\r\n// you may not use this file except in compliance with the License.\r\n// You may obtain a copy of the License at\r\n//\r\n// http://www.rockrms.com/license\r\n//\r\n// Unless required by applicable law or agreed to in writing, software\r\n// distributed under the License is distributed on an \"AS IS\" BASIS,\r\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n// See the License for the specific language governing permissions and\r\n// limitations under the License.\r\n// \r\n//\r\n\r\nimport { CancellationTokenSource, ICancellationToken } from \"./cancellation\";\r\nimport { isHTMLElement } from \"./dom\";\r\n\r\n/**\r\n * Compares two values for equality by performing deep nested comparisons\r\n * if the values are objects or arrays.\r\n *\r\n * @param a The first value to compare.\r\n * @param b The second value to compare.\r\n * @param strict True if strict comparision is required (meaning 1 would not equal \"1\").\r\n *\r\n * @returns True if the two values are equal to each other.\r\n */\r\nexport function deepEqual(a: unknown, b: unknown, strict: boolean): boolean {\r\n // Catches everything but objects.\r\n if (strict && a === b) {\r\n return true;\r\n }\r\n else if (!strict && a == b) {\r\n return true;\r\n }\r\n\r\n // NaN never equals another NaN, but functionally they are the same.\r\n if (typeof a === \"number\" && typeof b === \"number\" && isNaN(a) && isNaN(b)) {\r\n return true;\r\n }\r\n\r\n // Remaining value types must both be of type object\r\n if (a && b && typeof a === \"object\" && typeof b === \"object\") {\r\n // Array status must match.\r\n if (Array.isArray(a) !== Array.isArray(b)) {\r\n return false;\r\n }\r\n\r\n if (Array.isArray(a) && Array.isArray(b)) {\r\n // Array lengths must match.\r\n if (a.length !== b.length) {\r\n return false;\r\n }\r\n\r\n // Each element in the array must match.\r\n for (let i = 0; i < a.length; i++) {\r\n if (!deepEqual(a[i], b[i], strict)) {\r\n return false;\r\n }\r\n }\r\n\r\n return true;\r\n }\r\n else {\r\n // NOTE: There are a few edge cases not accounted for here, but they\r\n // are rare and unusual:\r\n // Map, Set, ArrayBuffer, RegExp\r\n\r\n // The objects must be of the same \"object type\".\r\n if (a.constructor !== b.constructor) {\r\n return false;\r\n }\r\n\r\n // The objects must be the same instance if they are both HTML elements.\r\n if (isHTMLElement(a) && isHTMLElement(b)) {\r\n return a === b;\r\n }\r\n\r\n // Get all the key/value pairs of each object and sort them so they\r\n // are in the same order as each other.\r\n const aEntries = Object.entries(a).sort((a, b) => a[0] < b[0] ? -1 : (a[0] > b[0] ? 1 : 0));\r\n const bEntries = Object.entries(b).sort((a, b) => a[0] < b[0] ? -1 : (a[0] > b[0] ? 1 : 0));\r\n\r\n // Key/value count must be identical.\r\n if (aEntries.length !== bEntries.length) {\r\n return false;\r\n }\r\n\r\n for (let i = 0; i < aEntries.length; i++) {\r\n const aEntry = aEntries[i];\r\n const bEntry = bEntries[i];\r\n\r\n // Ensure the keys are equal, must always be strict.\r\n if (!deepEqual(aEntry[0], bEntry[0], true)) {\r\n return false;\r\n }\r\n\r\n // Ensure the values are equal.\r\n if (!deepEqual(aEntry[1], bEntry[1], strict)) {\r\n return false;\r\n }\r\n }\r\n\r\n return true;\r\n }\r\n }\r\n\r\n return false;\r\n}\r\n\r\n\r\n/**\r\n * Debounces the function so it will only be called once during the specified\r\n * delay period. The returned function should be called to trigger the original\r\n * function that is to be debounced.\r\n *\r\n * @param fn The function to be called once per delay period.\r\n * @param delay The period in milliseconds. If the returned function is called\r\n * more than once during this period then fn will only be executed once for\r\n * the period. If not specified then it defaults to 250ms.\r\n * @param eager If true then the fn function will be called immediately and\r\n * then any subsequent calls will be debounced.\r\n *\r\n * @returns A function to be called when fn should be executed.\r\n */\r\nexport function debounce(fn: (() => void), delay: number = 250, eager: boolean = false): (() => void) {\r\n let timeout: NodeJS.Timeout | null = null;\r\n\r\n return (): void => {\r\n if (timeout) {\r\n clearTimeout(timeout);\r\n }\r\n else if (eager) {\r\n // If there was no previous timeout and we are configured for\r\n // eager calls, then execute now.\r\n fn();\r\n\r\n // An eager call should not result in a final debounce call.\r\n timeout = setTimeout(() => timeout = null, delay);\r\n\r\n return;\r\n }\r\n\r\n // If we had a previous timeout or we are not set for eager calls\r\n // then set a timeout to initiate the function after the delay.\r\n timeout = setTimeout(() => {\r\n timeout = null;\r\n fn();\r\n }, delay);\r\n };\r\n}\r\n\r\n/**\r\n * Options for debounceAsync function\r\n */\r\ntype DebounceAsyncOptions = {\r\n /**\r\n * The period in milliseconds. If the returned function is called more than\r\n * once during this period, then `fn` will only be executed once for the\r\n * period.\r\n * @default 250\r\n */\r\n delay?: number;\r\n\r\n /**\r\n * If `true`, then the `fn` function will be called immediately on the first\r\n * call, and any subsequent calls will be debounced.\r\n * @default false\r\n */\r\n eager?: boolean;\r\n};\r\n\r\n/**\r\n * Debounces the function so it will only be called once during the specified\r\n * delay period. The returned function should be called to trigger the original\r\n * function that is to be debounced.\r\n *\r\n * **Note:** Due to the asynchronous nature of JavaScript and how promises work,\r\n * `fn` may be invoked multiple times before previous invocations have completed.\r\n * To ensure that only the latest invocation proceeds and to prevent stale data,\r\n * you should check `cancellationToken.isCancellationRequested` at appropriate\r\n * points within your `fn` implementation—ideally after you `await` data from the\r\n * server. If cancellation is requested, `fn` should promptly abort execution.\r\n *\r\n * @param fn The function to be called once per delay period.\r\n * @param options An optional object specifying debounce options.\r\n *\r\n * @returns A function to be called when `fn` should be executed. This function\r\n * accepts an optional `parentCancellationToken` that, when canceled, will also\r\n * cancel the execution of `fn`.\r\n */\r\nexport function debounceAsync(\r\n fn: ((cancellationToken: ICancellationToken) => PromiseLike),\r\n options?: DebounceAsyncOptions\r\n): ((parentCancellationToken?: ICancellationToken) => Promise) {\r\n const delay = options?.delay ?? 250;\r\n const eager = options?.eager ?? false;\r\n\r\n let timeout: NodeJS.Timeout | null = null;\r\n let source: CancellationTokenSource | null = null;\r\n let isEagerExecutionInProgress = false;\r\n let pendingPromise: PromiseLike | null = null;\r\n\r\n return async (parentCancellationToken?: ICancellationToken): Promise => {\r\n // Always cancel any ongoing execution of fn.\r\n source?.cancel();\r\n\r\n if (timeout) {\r\n clearTimeout(timeout);\r\n timeout = null;\r\n }\r\n else if (eager && !isEagerExecutionInProgress) {\r\n // Execute immediately on the first call.\r\n isEagerExecutionInProgress = true;\r\n source = new CancellationTokenSource(parentCancellationToken);\r\n\r\n // Set the timeout before awaiting fn.\r\n timeout = setTimeout(() => {\r\n timeout = null;\r\n isEagerExecutionInProgress = false;\r\n }, delay);\r\n\r\n try {\r\n pendingPromise = fn(source.token);\r\n return await pendingPromise;\r\n }\r\n catch (e) {\r\n console.error(e || \"Unknown error while debouncing async function call.\");\r\n throw e;\r\n }\r\n }\r\n\r\n // Schedule the function to run after the delay.\r\n source = new CancellationTokenSource(parentCancellationToken);\r\n const cts = source;\r\n\r\n return new Promise((resolve, reject) => {\r\n timeout = setTimeout(async () => {\r\n try {\r\n pendingPromise = fn(cts.token);\r\n resolve(await pendingPromise);\r\n }\r\n catch (e) {\r\n console.error(e || \"Unknown error while debouncing async function call.\");\r\n reject(e);\r\n }\r\n\r\n timeout = null;\r\n isEagerExecutionInProgress = false;\r\n }, delay);\r\n });\r\n };\r\n}\r\n\r\n/**\r\n * Returns `true` if the value is `null` or `undefined` (nullish).\r\n */\r\nexport function isNullish(value: unknown): value is null | undefined {\r\n return (value ?? null) === null;\r\n}\r\n\r\n/**\r\n * Returns `true` if the value is not `null` or `undefined` (nullish).\r\n */\r\nexport function isNotNullish(item: T): item is NonNullable {\r\n return !isNullish(item);\r\n}","import { Guid } from \"@Obsidian/Types\";\r\nimport { BlockBeginEditData, BlockEndEditData, BrowserBusCallback, BrowserBusOptions, Message, QueryStringChangedData } from \"@Obsidian/Types/Utility/browserBus\";\r\nimport { areEqual } from \"./guid\";\r\n\r\n/*\r\n * READ THIS BEFORE MAKING ANY CHANGES TO THE BUS.\r\n *\r\n * OVERVIEW\r\n *\r\n * The browser bus is a basic pubsub interface within a single page. If you\r\n * publish a message to one instance of the bus it will be available to any\r\n * other instance on the same page. This uses document.addEventListener()\r\n * and document.dispatchEvent() with a single custom event name of `rockMessage`.\r\n *\r\n * The browser bus will not communicate with other browsers on the same page or\r\n * even other tabs within the same browser.\r\n *\r\n * For full documentation, see the gitbook developer documentation.\r\n *\r\n * FRAMEWORK MESSAGES\r\n *\r\n * All \"framework\" messages should have a type defined in\r\n * @Obsidian/Types/Utility/browserBus that specify the data type expected. If\r\n * no data type is expected than `void` can be used as the type. Message data\r\n * should always be an object rather than a primitive. This allows us to add\r\n * additional values without it being a breaking change to existing code.\r\n *\r\n * Additionally, all framework messages should have their name defined in either\r\n * the PageMessages object or BlockMessages object. This is for uniformity so it\r\n * is easier for core code and plugins to subscribe to these messages and know\r\n * they got the right message name.\r\n *\r\n * SUBSCRIBE OVERLOADS\r\n *\r\n * When adding new framework messages, be sure to add overloads to the\r\n * subscribe, subscribeToBlock and subscribeToBlockType functions for that\r\n * message name and data type. This compiles away to nothing but provides a\r\n * much better TypeScript experience.\r\n */\r\n\r\n\r\n/**\r\n * Framework messages that will be sent for pages.\r\n */\r\nexport const PageMessages = {\r\n /**\r\n * Sent when the query string is changed outside the context of a page load.\r\n */\r\n QueryStringChanged: \"page.core.queryStringChanged\"\r\n} as const;\r\n\r\n/**\r\n * Framework messages that will be sent for blocks.\r\n */\r\nexport const BlockMessages = {\r\n /**\r\n * Sent just before a block switches into edit mode.\r\n */\r\n BeginEdit: \"block.core.beginEdit\",\r\n\r\n /**\r\n * Sent just after a block switches out of edit mode.\r\n */\r\n EndEdit: \"block.core.endEdit\",\r\n} as const;\r\n\r\n/**\r\n * Gets an object that will provide access to the browser bus. This bus will\r\n * allow different code on the page to send and receive messages betwen each\r\n * other as well as plain JavaScript. This bus does not cross page boundaries.\r\n *\r\n * Meaning, if you publish a message in one tab it will not show up in another\r\n * tab in the same (or a different) browser. Neither will messages magically\r\n * persist across page loads.\r\n *\r\n * If you call this method you are responsible for calling the {@link BrowserBus.dispose}\r\n * function when you are done with the bus. If you do not then your component\r\n * will probably never be garbage collected and your subscribed event handlers\r\n * will continue to be called.\r\n *\r\n * @param options Custom options to construct the {@link BrowserBus} object with. This should normally not be needed.\r\n *\r\n * @returns The object that provides access to the browser bus.\r\n */\r\nexport function useBrowserBus(options?: BrowserBusOptions): BrowserBus {\r\n return new BrowserBus(options ?? {});\r\n}\r\n\r\n// #region Internal Types\r\n\r\n/**\r\n * Internal message handler state that includes the filters used to decide\r\n * if the callback is valid for the message.\r\n */\r\ntype MessageHandler = {\r\n /** If not nullish messages must match this message name. */\r\n name?: string;\r\n\r\n /** If not nullish then messages must be from this block type. */\r\n blockType?: Guid;\r\n\r\n /** If not nullish them messages must be from this block instance. */\r\n block?: Guid;\r\n\r\n /** The callback that will be called. */\r\n callback: BrowserBusCallback;\r\n};\r\n\r\n// #endregion\r\n\r\n// #region Internal Implementation\r\n\r\n/** This is the JavaScript event name we use with dispatchEvent(). */\r\nconst customDomEventName = \"rockMessage\";\r\n\r\n/**\r\n * The main browser bus implementation. This uses a shared method to publish\r\n * and subscribe to messages such that if you create two BrowserBus instances on\r\n * the same page they will still be able to talk to each other.\r\n *\r\n * However, they will not be able to talk to instances on other pages such as\r\n * in other browser tabs.\r\n */\r\nexport class BrowserBus {\r\n /** The registered handlers that will potentially be invoked. */\r\n private handlers: MessageHandler[] = [];\r\n\r\n /** The options we were created with. */\r\n private options: BrowserBusOptions;\r\n\r\n /** The event listener. Used so we can remove the listener later. */\r\n private eventListener: (e: Event) => void;\r\n\r\n /**\r\n * Creates a new instance of the bus and prepares it to receive messages.\r\n *\r\n * This should be considered an internal constructor and not used by plugins.\r\n *\r\n * @param options The options that describe how this instance should operate.\r\n */\r\n constructor(options: BrowserBusOptions) {\r\n this.options = { ...options };\r\n\r\n this.eventListener = e => this.onEvent(e);\r\n document.addEventListener(customDomEventName, this.eventListener);\r\n }\r\n\r\n // #region Private Functions\r\n\r\n /**\r\n * Called when an event is received from the document listener.\r\n *\r\n * @param event The low level JavaScript even that was received.\r\n */\r\n private onEvent(event: Event): void {\r\n if (!(event instanceof CustomEvent)) {\r\n return;\r\n }\r\n\r\n let message = event.detail as Message;\r\n\r\n // Discard the message if it is not valid.\r\n if (!message.name) {\r\n return;\r\n }\r\n\r\n // If we got a message without a timestamp, it probably came from\r\n // plain JavaScript, so set it to 0.\r\n if (typeof message.timestamp === \"undefined\") {\r\n message = { ...message, timestamp: 0 };\r\n }\r\n\r\n this.onMessage(message);\r\n }\r\n\r\n /**\r\n * Called when a browser bus message is received from the bus.\r\n *\r\n * @param message The message that was received.\r\n */\r\n private onMessage(message: Message): void {\r\n // Make a copy of the handlers in case our list of handlers if modified\r\n // inside a handler.\r\n const handlers = [...this.handlers];\r\n\r\n for (const handler of handlers) {\r\n try {\r\n // Perform all the filtering. We could do this all in one\r\n // line but this is easier to read and understand.\r\n if (handler.name && handler.name !== message.name) {\r\n continue;\r\n }\r\n\r\n if (handler.blockType && !areEqual(handler.blockType, message.blockType)) {\r\n continue;\r\n }\r\n\r\n if (handler.block && !areEqual(handler.block, message.block)) {\r\n continue;\r\n }\r\n\r\n // All filters passed, execute the callback.\r\n handler.callback(message);\r\n }\r\n catch (e) {\r\n // Catch the error and display it so other handlers will still\r\n // be checked and called.\r\n console.error(e);\r\n }\r\n }\r\n }\r\n\r\n // #endregion\r\n\r\n // #region Public Functions\r\n\r\n /**\r\n * Frees up any resources used by this browser bus instance.\r\n */\r\n public dispose(): void {\r\n document.removeEventListener(customDomEventName, this.eventListener);\r\n this.handlers.splice(0, this.handlers.length);\r\n }\r\n\r\n /**\r\n * Publishes a named message without any data.\r\n *\r\n * @param messageName The name of the message to publish.\r\n */\r\n public publish(messageName: string): void;\r\n\r\n /**\r\n * Publishes a named message with some custom data.\r\n *\r\n * @param messageName The name of the message to publish.\r\n * @param data The custom data to include with the message.\r\n */\r\n public publish(messageName: string, data: unknown): void;\r\n\r\n /**\r\n * Publishes a named message with some custom data.\r\n *\r\n * @param messageName The name of the message to publish.\r\n * @param data The custom data to include with the message.\r\n */\r\n public publish(messageName: string, data?: unknown): void {\r\n this.publishMessage({\r\n name: messageName,\r\n timestamp: Date.now(),\r\n blockType: this.options.blockType,\r\n block: this.options.block,\r\n data\r\n });\r\n }\r\n\r\n /**\r\n * Publishes a message to the browser bus. No changes are made to the\r\n * message object.\r\n *\r\n * Do not use this message to publish a block message unless you have\r\n * manually filled in the {@link Message.blockType} and\r\n * {@link Message.block} properties.\r\n *\r\n * @param message The message to publish.\r\n */\r\n public publishMessage(message: Message): void {\r\n const event = new CustomEvent(customDomEventName, {\r\n detail: message\r\n });\r\n\r\n document.dispatchEvent(event);\r\n }\r\n\r\n // #endregion\r\n\r\n // #region subscribe()\r\n\r\n /**\r\n * Subscribes to the named message from any source.\r\n *\r\n * @param messageName The name of the message to subscribe to.\r\n * @param callback The callback to invoke when the message is received.\r\n */\r\n public subscribe(messageName: string, callback: BrowserBusCallback): void;\r\n\r\n /**\r\n * Subscribes to the named message from any source.\r\n *\r\n * @param messageName The name of the message to subscribe to.\r\n * @param callback The callback to invoke when the message is received.\r\n */\r\n public subscribe(messageName: \"page.core.queryStringChanged\", callback: BrowserBusCallback): void;\r\n\r\n /**\r\n * Subscribes to the named message from any source.\r\n *\r\n * @param messageName The name of the message to subscribe to.\r\n * @param callback The callback to invoke when the message is received.\r\n */\r\n public subscribe(messageName: \"block.core.beginEdit\", callback: BrowserBusCallback): void;\r\n\r\n /**\r\n * Subscribes to the named message from any source.\r\n *\r\n * @param messageName The name of the message to subscribe to.\r\n * @param callback The callback to invoke when the message is received.\r\n */\r\n public subscribe(messageName: \"block.core.endEdit\", callback: BrowserBusCallback): void;\r\n\r\n /**\r\n * Subscribes to any message that is sent.\r\n *\r\n * @param callback The callback to invoke when the message is received.\r\n */\r\n public subscribe(callback: BrowserBusCallback): void;\r\n\r\n /**\r\n * Subscribes to messages from any source.\r\n *\r\n * @param messageNameOrCallback The name of the message to subscribe to or the callback.\r\n * @param callback The callback to invoke when the message is received.\r\n */\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n public subscribe(messageNameOrCallback: string | BrowserBusCallback, callback?: BrowserBusCallback): void {\r\n let name: string | undefined;\r\n\r\n if (typeof messageNameOrCallback === \"string\") {\r\n name = messageNameOrCallback;\r\n }\r\n else {\r\n name = undefined;\r\n callback = messageNameOrCallback;\r\n }\r\n\r\n if (!callback) {\r\n return;\r\n }\r\n\r\n this.handlers.push({\r\n name,\r\n callback\r\n });\r\n }\r\n\r\n // #endregion\r\n\r\n // #region subscribeToBlockType()\r\n\r\n /**\r\n * Subscribes to the named message from any block instance with a matching\r\n * block type identifier.\r\n *\r\n * @param messageName The name of the message to subscribe to.\r\n * @param blockType The identifier of the block type.\r\n * @param callback The callback to invoke when the message is received.\r\n */\r\n public subscribeToBlockType(messageName: string, blockType: Guid, callback: BrowserBusCallback): void;\r\n\r\n /**\r\n * Subscribes to the named message from any block instance with a matching\r\n * block type identifier.\r\n *\r\n * @param messageName The name of the message to subscribe to.\r\n * @param blockType The identifier of the block type.\r\n * @param callback The callback to invoke when the message is received.\r\n */\r\n public subscribeToBlockType(messageName: \"block.core.beginEdit\", blockType: Guid, callback: BrowserBusCallback): void;\r\n\r\n /**\r\n * Subscribes to the named message from any block instance with a matching\r\n * block type identifier.\r\n *\r\n * @param messageName The name of the message to subscribe to.\r\n * @param blockType The identifier of the block type.\r\n * @param callback The callback to invoke when the message is received.\r\n */\r\n public subscribeToBlockType(messageName: \"block.core.endEdit\", blockType: Guid, callback: BrowserBusCallback): void;\r\n\r\n /**\r\n * Subscribes to any message that is sent from any block instance with a\r\n * matching block type identifier.\r\n *\r\n * @param blockType The identifier of the block type.\r\n * @param callback The callback to invoke when the message is received.\r\n */\r\n public subscribeToBlockType(blockType: Guid, callback: BrowserBusCallback): void;\r\n\r\n /**\r\n * Subscribes to messages from any block instance with a matching block\r\n * type identifier.\r\n *\r\n * @param messageNameOrBlockType The name of the message to subscribe to or the block type.\r\n * @param blockTypeOrCallback The block type or the callback function.\r\n * @param callback The callback to invoke when the message is received.\r\n */\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n public subscribeToBlockType(messageNameOrBlockType: string | Guid, blockTypeOrCallback: Guid | BrowserBusCallback, callback?: BrowserBusCallback): void {\r\n let name: string | undefined;\r\n let blockType: Guid;\r\n\r\n if (typeof blockTypeOrCallback === \"string\") {\r\n name = messageNameOrBlockType;\r\n blockType = blockTypeOrCallback;\r\n }\r\n else {\r\n blockType = messageNameOrBlockType;\r\n callback = blockTypeOrCallback;\r\n }\r\n\r\n if (!blockType || !callback) {\r\n return;\r\n }\r\n\r\n this.handlers.push({\r\n name,\r\n blockType,\r\n callback\r\n });\r\n }\r\n\r\n // #endregion\r\n\r\n // #region subscribeToBlock()\r\n\r\n /**\r\n * Subscribes to the named message from a single block instance.\r\n *\r\n * @param messageName The name of the message to subscribe to.\r\n * @param block The identifier of the block.\r\n * @param callback The callback to invoke when the message is received.\r\n */\r\n public subscribeToBlock(messageName: string, block: Guid, callback: BrowserBusCallback): void;\r\n\r\n /**\r\n * Subscribes to the named message from a single block instance.\r\n *\r\n * @param messageName The name of the message to subscribe to.\r\n * @param block The identifier of the block.\r\n * @param callback The callback to invoke when the message is received.\r\n */\r\n public subscribeToBlock(messageName: \"block.core.beginEdit\", block: Guid, callback: BrowserBusCallback): void;\r\n\r\n /**\r\n * Subscribes to the named message from a single block instance.\r\n *\r\n * @param messageName The name of the message to subscribe to.\r\n * @param block The identifier of the block.\r\n * @param callback The callback to invoke when the message is received.\r\n */\r\n public subscribeToBlock(messageName: \"block.core.endEdit\", block: Guid, callback: BrowserBusCallback): void;\r\n\r\n /**\r\n * Subscribes to any message that is sent from a single block instance.\r\n *\r\n * @param block The identifier of the block.\r\n * @param callback The callback to invoke when the message is received.\r\n */\r\n public subscribeToBlock(block: Guid, callback: BrowserBusCallback): void;\r\n\r\n /**\r\n * Subscribes to messages from a single block instance.\r\n *\r\n * @param messageNameOrBlock The name of the message to subscribe to or the block.\r\n * @param blockOrCallback The block or the callback function.\r\n * @param callback The callback to invoke when the message is received.\r\n */\r\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\r\n public subscribeToBlock(messageNameOrBlock: string | Guid, blockOrCallback: Guid | BrowserBusCallback, callback?: BrowserBusCallback): void {\r\n let name: string | undefined;\r\n let block: Guid;\r\n\r\n if (typeof blockOrCallback === \"string\") {\r\n name = messageNameOrBlock;\r\n block = blockOrCallback;\r\n }\r\n else {\r\n block = messageNameOrBlock;\r\n callback = blockOrCallback;\r\n }\r\n\r\n if (!block || !callback) {\r\n return;\r\n }\r\n\r\n this.handlers.push({\r\n name,\r\n block,\r\n callback\r\n });\r\n }\r\n\r\n // #endregion\r\n}\r\n\r\n// #endregion\r\n","// \r\n// Copyright by the Spark Development Network\r\n//\r\n// Licensed under the Rock Community License (the \"License\");\r\n// you may not use this file except in compliance with the License.\r\n// You may obtain a copy of the License at\r\n//\r\n// http://www.rockrms.com/license\r\n//\r\n// Unless required by applicable law or agreed to in writing, software\r\n// distributed under the License is distributed on an \"AS IS\" BASIS,\r\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n// See the License for the specific language governing permissions and\r\n// limitations under the License.\r\n// \r\n//\r\n\r\nimport { BlockEvent, InvokeBlockActionFunc, SecurityGrant } from \"@Obsidian/Types/Utility/block\";\r\nimport { IBlockPersonPreferencesProvider, IPersonPreferenceCollection } from \"@Obsidian/Types/Core/personPreferences\";\r\nimport { ExtendedRef } from \"@Obsidian/Types/Utility/component\";\r\nimport { DetailBlockBox } from \"@Obsidian/ViewModels/Blocks/detailBlockBox\";\r\nimport { inject, provide, Ref, ref, watch } from \"vue\";\r\nimport { RockDateTime } from \"./rockDateTime\";\r\nimport { Guid } from \"@Obsidian/Types\";\r\nimport { HttpBodyData, HttpPostFunc, HttpResult } from \"@Obsidian/Types/Utility/http\";\r\nimport { BlockActionContextBag } from \"@Obsidian/ViewModels/Blocks/blockActionContextBag\";\r\nimport { ValidPropertiesBox } from \"@Obsidian/ViewModels/Utility/validPropertiesBox\";\r\nimport { IEntity } from \"@Obsidian/ViewModels/entity\";\r\nimport { debounce } from \"./util\";\r\nimport { BrowserBus, useBrowserBus } from \"./browserBus\";\r\nimport { ICancellationToken } from \"./cancellation\";\r\n\r\nconst blockReloadSymbol = Symbol();\r\nconst configurationValuesChangedSymbol = Symbol();\r\nconst staticContentSymbol = Symbol(\"static-content\");\r\nconst blockBrowserBusSymbol = Symbol(\"block-browser-bus\");\r\n\r\n// TODO: Change these to use symbols\r\n\r\n/**\r\n * Maps the block configuration values to the expected type.\r\n *\r\n * @returns The configuration values for the block.\r\n */\r\nexport function useConfigurationValues(): T {\r\n const result = inject[>(\"configurationValues\");\r\n\r\n if (result === undefined) {\r\n throw \"Attempted to access block configuration outside of a RockBlock.\";\r\n }\r\n\r\n return result.value;\r\n}\r\n\r\n/**\r\n * Gets the function that will be used to invoke block actions.\r\n *\r\n * @returns An instance of @see {@link InvokeBlockActionFunc}.\r\n */\r\nexport function useInvokeBlockAction(): InvokeBlockActionFunc {\r\n const result = inject(\"invokeBlockAction\");\r\n\r\n if (result === undefined) {\r\n throw \"Attempted to access block action invocation outside of a RockBlock.\";\r\n }\r\n\r\n return result;\r\n}\r\n\r\n/**\r\n * Gets the function that will return the URL for a block action.\r\n *\r\n * @returns A function that can be called to determine the URL for a block action.\r\n */\r\nexport function useBlockActionUrl(): (actionName: string) => string {\r\n const result = inject<(actionName: string) => string>(\"blockActionUrl\");\r\n\r\n if (result === undefined) {\r\n throw \"Attempted to access block action URL outside of a RockBlock.\";\r\n }\r\n\r\n return result;\r\n}\r\n\r\n/**\r\n * Creates a function that can be provided to the block that allows calling\r\n * block actions.\r\n *\r\n * @private This should not be used by plugins.\r\n *\r\n * @param post The function to handle the post operation.\r\n * @param pageGuid The unique identifier of the page.\r\n * @param blockGuid The unique identifier of the block.\r\n * @param pageParameters The parameters to include with the block action calls.\r\n *\r\n * @returns A function that can be used to provide the invoke block action.\r\n */\r\nexport function createInvokeBlockAction(post: HttpPostFunc, pageGuid: Guid, blockGuid: Guid, pageParameters: Record, interactionGuid: Guid): InvokeBlockActionFunc {\r\n async function invokeBlockAction(actionName: string, data: HttpBodyData | undefined = undefined, actionContext: BlockActionContextBag | undefined = undefined, cancellationToken?: ICancellationToken): Promise> {\r\n let context: BlockActionContextBag = {};\r\n\r\n if (actionContext) {\r\n context = { ...actionContext };\r\n }\r\n\r\n context.pageParameters = pageParameters;\r\n context.interactionGuid = interactionGuid;\r\n\r\n return await post(\r\n `/api/v2/BlockActions/${pageGuid}/${blockGuid}/${actionName}`,\r\n undefined,\r\n {\r\n __context: context,\r\n ...data\r\n },\r\n cancellationToken);\r\n }\r\n\r\n return invokeBlockAction;\r\n}\r\n\r\n/**\r\n * Provides the reload block callback function for a block. This is an internal\r\n * method and should not be used by plugins.\r\n *\r\n * @param callback The callback that will be called when a block wants to reload itself.\r\n */\r\nexport function provideReloadBlock(callback: () => void): void {\r\n provide(blockReloadSymbol, callback);\r\n}\r\n\r\n/**\r\n * Gets a function that can be called when a block wants to reload itself.\r\n *\r\n * @returns A function that will cause the block component to be reloaded.\r\n */\r\nexport function useReloadBlock(): () => void {\r\n return inject<() => void>(blockReloadSymbol, () => {\r\n // Intentionally blank, do nothing by default.\r\n });\r\n}\r\n\r\n/**\r\n * Provides the data for a block to be notified when its configuration values\r\n * have changed. This is an internal method and should not be used by plugins.\r\n *\r\n * @returns An object with an invoke and reset function.\r\n */\r\nexport function provideConfigurationValuesChanged(): { invoke: () => void, reset: () => void } {\r\n const callbacks: (() => void)[] = [];\r\n\r\n provide(configurationValuesChangedSymbol, callbacks);\r\n\r\n return {\r\n invoke: (): void => {\r\n for (const c of callbacks) {\r\n c();\r\n }\r\n },\r\n\r\n reset: (): void => {\r\n callbacks.splice(0, callbacks.length);\r\n }\r\n };\r\n}\r\n\r\n/**\r\n * Registered a function to be called when the block configuration values have\r\n * changed.\r\n *\r\n * @param callback The function to be called when the configuration values have changed.\r\n */\r\nexport function onConfigurationValuesChanged(callback: () => void): void {\r\n const callbacks = inject<(() => void)[]>(configurationValuesChangedSymbol);\r\n\r\n if (callbacks !== undefined) {\r\n callbacks.push(callback);\r\n }\r\n}\r\n\r\n/**\r\n * Provides the static content that the block provided on the server.\r\n *\r\n * @param content The static content from the server.\r\n */\r\nexport function provideStaticContent(content: Ref): void {\r\n provide(staticContentSymbol, content);\r\n}\r\n\r\n/**\r\n * Gets the static content that was provided by the block on the server.\r\n *\r\n * @returns A string of HTML content or undefined.\r\n */\r\nexport function useStaticContent(): Node[] {\r\n const content = inject][>(staticContentSymbol);\r\n\r\n if (!content) {\r\n return [];\r\n }\r\n\r\n return content.value;\r\n}\r\n\r\n/**\r\n * Provides the browser bus configured to publish messages for the current\r\n * block.\r\n *\r\n * @param bus The browser bus.\r\n */\r\nexport function provideBlockBrowserBus(bus: BrowserBus): void {\r\n provide(blockBrowserBusSymbol, bus);\r\n}\r\n\r\n/**\r\n * Gets the browser bus configured for use by the current block. If available\r\n * this will be properly configured to publish messages with the correct block\r\n * and block type. If this is called outside the context of a block then a\r\n * generic use {@link BrowserBus} will be returned.\r\n *\r\n * @returns An instance of {@link BrowserBus}.\r\n */\r\nexport function useBlockBrowserBus(): BrowserBus {\r\n return inject(blockBrowserBusSymbol, () => useBrowserBus(), true);\r\n}\r\n\r\n\r\n/**\r\n * A type that returns the keys of a child property.\r\n */\r\ntype ChildKeys, PropertyName extends string> = keyof NonNullable & string;\r\n\r\n/**\r\n * A valid properties box that uses the specified name for the content bag.\r\n */\r\ntype ValidPropertiesSettingsBox = {\r\n validProperties?: string[] | null;\r\n} & {\r\n settings?: Record | null;\r\n};\r\n\r\n/**\r\n * Sets the a value for a custom settings box. This will set the value and then\r\n * add the property name to the list of valid properties.\r\n *\r\n * @param box The box whose custom setting value will be set.\r\n * @param propertyName The name of the custom setting property to set.\r\n * @param value The new value of the custom setting.\r\n */\r\nexport function setCustomSettingsBoxValue, K extends ChildKeys>(box: T, propertyName: K, value: S[K]): void {\r\n if (!box.settings) {\r\n box.settings = {} as Record;\r\n }\r\n\r\n box.settings[propertyName] = value;\r\n\r\n if (!box.validProperties) {\r\n box.validProperties = [];\r\n }\r\n\r\n if (!box.validProperties.includes(propertyName)) {\r\n box.validProperties.push(propertyName);\r\n }\r\n}\r\n\r\n/**\r\n * Sets the a value for a property box. This will set the value and then\r\n * add the property name to the list of valid properties.\r\n *\r\n * @param box The box whose property value will be set.\r\n * @param propertyName The name of the property on the bag to set.\r\n * @param value The new value of the property.\r\n */\r\nexport function setPropertiesBoxValue, K extends keyof T & string>(box: ValidPropertiesBox, propertyName: K, value: T[K]): void {\r\n if (!box.bag) {\r\n box.bag = {} as Record as T;\r\n }\r\n\r\n box.bag[propertyName] = value;\r\n\r\n if (!box.validProperties) {\r\n box.validProperties = [];\r\n }\r\n\r\n if (!box.validProperties.some(p => p.toLowerCase() === propertyName.toLowerCase())) {\r\n box.validProperties.push(propertyName);\r\n }\r\n}\r\n\r\n/**\r\n * Dispatches a block event to the document.\r\n *\r\n * @deprecated Do not use this function anymore, it will be removed in the future.\r\n * Use the BrowserBus instead.\r\n *\r\n * @param eventName The name of the event to be dispatched.\r\n * @param eventData The custom data to be attached to the event.\r\n *\r\n * @returns true if preventDefault() was called on the event, otherwise false.\r\n */\r\nexport function dispatchBlockEvent(eventName: string, blockGuid: Guid, eventData?: unknown): boolean {\r\n const ev = new CustomEvent(eventName, {\r\n cancelable: true,\r\n detail: {\r\n guid: blockGuid,\r\n data: eventData\r\n }\r\n });\r\n\r\n return document.dispatchEvent(ev);\r\n}\r\n\r\n/**\r\n * Tests if the given event is a custom block event. This does not ensure\r\n * that the event data is the correct type, only the event itself.\r\n *\r\n * @param event The event to be tested.\r\n *\r\n * @returns true if the event is a block event.\r\n */\r\nexport function isBlockEvent(event: Event): event is CustomEvent> {\r\n return event instanceof CustomEvent\r\n && typeof event.detail === \"object\"\r\n && \"guid\" in event.detail\r\n && \"data\" in event.detail;\r\n}\r\n\r\n// #region Entity Detail Blocks\r\n\r\nconst entityTypeNameSymbol = Symbol(\"EntityTypeName\");\r\nconst entityTypeGuidSymbol = Symbol(\"EntityTypeGuid\");\r\n\r\ntype UseEntityDetailBlockOptions = {\r\n /** The block configuration. */\r\n blockConfig: Record;\r\n\r\n /**\r\n * The entity that will be used by the block, this will cause the\r\n * onPropertyChanged logic to be generated.\r\n */\r\n entity?: Ref>;\r\n};\r\n\r\ntype UseEntityDetailBlockResult = {\r\n /** The onPropertyChanged handler for the edit panel. */\r\n onPropertyChanged?(propertyName: string): void;\r\n};\r\n\r\n/**\r\n * Performs any framework-level initialization of an entity detail block.\r\n *\r\n * @param options The options to use when initializing the detail block logic.\r\n *\r\n * @returns An object that contains information which can be used by the block.\r\n */\r\nexport function useEntityDetailBlock(options: UseEntityDetailBlockOptions): UseEntityDetailBlockResult {\r\n const securityGrant = getSecurityGrant(options.blockConfig.securityGrantToken as string);\r\n\r\n provideSecurityGrant(securityGrant);\r\n\r\n if (options.blockConfig.entityTypeName) {\r\n provideEntityTypeName(options.blockConfig.entityTypeName as string);\r\n }\r\n\r\n if (options.blockConfig.entityTypeGuid) {\r\n provideEntityTypeGuid(options.blockConfig.entityTypeGuid as Guid);\r\n }\r\n\r\n const entity = options.entity;\r\n\r\n const result: Record = {};\r\n\r\n if (entity) {\r\n const invokeBlockAction = useInvokeBlockAction();\r\n const refreshAttributesDebounce = debounce(() => refreshEntityDetailAttributes(entity, invokeBlockAction), undefined, true);\r\n\r\n result.onPropertyChanged = (propertyName: string): void => {\r\n // If we don't have any qualified attribute properties or this property\r\n // is not one of them then do nothing.\r\n if (!options.blockConfig.qualifiedAttributeProperties || !(options.blockConfig.qualifiedAttributeProperties as string[]).some(n => n.toLowerCase() === propertyName.toLowerCase())) {\r\n return;\r\n }\r\n\r\n refreshAttributesDebounce();\r\n };\r\n }\r\n\r\n return result;\r\n}\r\n\r\n/**\r\n * Provides the entity type name to child components.\r\n *\r\n * @param name The entity type name in PascalCase, such as `GroupMember`.\r\n */\r\nexport function provideEntityTypeName(name: string): void {\r\n provide(entityTypeNameSymbol, name);\r\n}\r\n\r\n/**\r\n * Gets the entity type name provided from a parent component.\r\n *\r\n * @returns The entity type name in PascalCase, such as `GroupMember` or undefined.\r\n */\r\nexport function useEntityTypeName(): string | undefined {\r\n return inject(entityTypeNameSymbol, undefined);\r\n}\r\n\r\n/**\r\n * Provides the entity type unique identifier to child components.\r\n *\r\n * @param guid The entity type unique identifier.\r\n */\r\nexport function provideEntityTypeGuid(guid: Guid): void {\r\n provide(entityTypeGuidSymbol, guid);\r\n}\r\n\r\n/**\r\n * Gets the entity type unique identifier provided from a parent component.\r\n *\r\n * @returns The entity type unique identifier or undefined.\r\n */\r\nexport function useEntityTypeGuid(): Guid | undefined {\r\n return inject(entityTypeGuidSymbol, undefined);\r\n}\r\n\r\n// #endregion\r\n\r\n// #region Security Grants\r\n\r\nconst securityGrantSymbol = Symbol();\r\n\r\n/**\r\n * Use a security grant token value provided by the server. This returns a reference\r\n * to the actual value and will automatically handle renewing the token and updating\r\n * the value. This function is meant to be used by blocks. Controls should use the\r\n * useSecurityGrant() function instead.\r\n *\r\n * @param token The token provided by the server.\r\n *\r\n * @returns A reference to the security grant that will be updated automatically when it has been renewed.\r\n */\r\nexport function getSecurityGrant(token: string | null | undefined): SecurityGrant {\r\n // Use || so that an empty string gets converted to null.\r\n const tokenRef = ref(token || null);\r\n const invokeBlockAction = useInvokeBlockAction();\r\n let renewalTimeout: NodeJS.Timeout | null = null;\r\n\r\n // Internal function to renew the token and re-schedule renewal.\r\n const renewToken = async (): Promise => {\r\n const result = await invokeBlockAction(\"RenewSecurityGrantToken\");\r\n\r\n if (result.isSuccess && result.data) {\r\n tokenRef.value = result.data;\r\n\r\n scheduleRenewal();\r\n }\r\n };\r\n\r\n // Internal function to schedule renewal based on the expiration date in\r\n // the existing token. Renewal happens 15 minutes before expiration.\r\n const scheduleRenewal = (): void => {\r\n // Cancel any existing renewal timer.\r\n if (renewalTimeout !== null) {\r\n clearTimeout(renewalTimeout);\r\n renewalTimeout = null;\r\n }\r\n\r\n // No token, nothing to do.\r\n if (tokenRef.value === null) {\r\n return;\r\n }\r\n\r\n const segments = tokenRef.value?.split(\";\");\r\n\r\n // Token not in expected format.\r\n if (segments.length !== 3 || segments[0] !== \"1\") {\r\n return;\r\n }\r\n\r\n const expiresDateTime = RockDateTime.parseISO(segments[1]);\r\n\r\n // Could not parse expiration date and time.\r\n if (expiresDateTime === null) {\r\n return;\r\n }\r\n\r\n const renewTimeout = expiresDateTime.addMinutes(-15).toMilliseconds() - RockDateTime.now().toMilliseconds();\r\n\r\n // Renewal request would be in the past, ignore.\r\n if (renewTimeout < 0) {\r\n return;\r\n }\r\n\r\n // Schedule the renewal task to happen 15 minutes before expiration.\r\n renewalTimeout = setTimeout(renewToken, renewTimeout);\r\n };\r\n\r\n scheduleRenewal();\r\n\r\n return {\r\n token: tokenRef,\r\n updateToken(newToken) {\r\n tokenRef.value = newToken || null;\r\n scheduleRenewal();\r\n }\r\n };\r\n}\r\n\r\n/**\r\n * Provides the security grant to child components to use in their API calls.\r\n *\r\n * @param grant The grant to provide to child components.\r\n */\r\nexport function provideSecurityGrant(grant: SecurityGrant): void {\r\n provide(securityGrantSymbol, grant);\r\n}\r\n\r\n/**\r\n * Uses a previously provided security grant token by a parent component.\r\n * This function is meant to be used by controls that need to obtain a security\r\n * grant from a parent component.\r\n *\r\n * @returns A string reference that contains the security grant token.\r\n */\r\nexport function useSecurityGrantToken(): Ref {\r\n const grant = inject(securityGrantSymbol);\r\n\r\n return grant ? grant.token : ref(null);\r\n}\r\n\r\n// #endregion\r\n\r\n// #region Extended References\r\n\r\n/** An emit object that conforms to having a propertyChanged event. */\r\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\r\nexport type PropertyChangedEmitFn = E extends Array ? (event: EE, ...args: any[]) => void : (event: E, ...args: any[]) => void;\r\n\r\n/**\r\n * Watches for changes to the given Ref objects and emits a special event to\r\n * indicate that a given property has changed.\r\n *\r\n * @param propertyRefs The ExtendedRef objects to watch for changes.\r\n * @param emit The emit function for the component.\r\n */\r\nexport function watchPropertyChanges(propertyRefs: ExtendedRef[], emit: PropertyChangedEmitFn): void {\r\n for (const propRef of propertyRefs) {\r\n watch(propRef, () => {\r\n if (propRef.context.propertyName) {\r\n emit(\"propertyChanged\", propRef.context.propertyName);\r\n }\r\n });\r\n }\r\n}\r\n\r\n/**\r\n * Requests an updated attribute list from the server based on the\r\n * current UI selections made.\r\n *\r\n * @param box The valid properties box that will be used to determine current\r\n * property values and then updated with the new attributes and values.\r\n * @param invokeBlockAction The function to use when calling the block action.\r\n */\r\nasync function refreshEntityDetailAttributes(box: Ref>, invokeBlockAction: InvokeBlockActionFunc): Promise {\r\n const result = await invokeBlockAction>(\"RefreshAttributes\", {\r\n box: box.value\r\n });\r\n\r\n if (result.isSuccess) {\r\n if (result.statusCode === 200 && result.data && box.value) {\r\n const newBox: ValidPropertiesBox = {\r\n ...box.value,\r\n bag: {\r\n ...box.value.bag as TEntityBag,\r\n attributes: result.data.bag?.attributes,\r\n attributeValues: result.data.bag?.attributeValues\r\n }\r\n };\r\n\r\n box.value = newBox;\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Requests an updated attribute list from the server based on the\r\n * current UI selections made.\r\n *\r\n * @param bag The entity bag that will be used to determine current property values\r\n * and then updated with the new attributes and values.\r\n * @param validProperties The properties that are considered valid on the bag when\r\n * the server will read the bag.\r\n * @param invokeBlockAction The function to use when calling the block action.\r\n */\r\nexport async function refreshDetailAttributes(bag: Ref, validProperties: string[], invokeBlockAction: InvokeBlockActionFunc): Promise {\r\n const data: DetailBlockBox = {\r\n entity: bag.value,\r\n isEditable: true,\r\n validProperties: validProperties\r\n };\r\n\r\n const result = await invokeBlockAction, unknown>>(\"RefreshAttributes\", {\r\n box: data\r\n });\r\n\r\n if (result.isSuccess) {\r\n if (result.statusCode === 200 && result.data && bag.value) {\r\n const newBag: TEntityBag = {\r\n ...bag.value,\r\n attributes: result.data.entity?.attributes,\r\n attributeValues: result.data.entity?.attributeValues\r\n };\r\n\r\n bag.value = newBag;\r\n }\r\n }\r\n}\r\n\r\n// #endregion Extended Refs\r\n\r\n// #region Block and BlockType Guid\r\n\r\nconst blockGuidSymbol = Symbol(\"block-guid\");\r\nconst blockTypeGuidSymbol = Symbol(\"block-type-guid\");\r\n\r\n/**\r\n * Provides the block unique identifier to all child components.\r\n * This is an internal method and should not be used by plugins.\r\n *\r\n * @param blockGuid The unique identifier of the block.\r\n */\r\nexport function provideBlockGuid(blockGuid: string): void {\r\n provide(blockGuidSymbol, blockGuid);\r\n}\r\n\r\n/**\r\n * Gets the unique identifier of the current block in this component chain.\r\n *\r\n * @returns The unique identifier of the block.\r\n */\r\nexport function useBlockGuid(): Guid | undefined {\r\n return inject(blockGuidSymbol);\r\n}\r\n\r\n/**\r\n * Provides the block type unique identifier to all child components.\r\n * This is an internal method and should not be used by plugins.\r\n *\r\n * @param blockTypeGuid The unique identifier of the block type.\r\n */\r\nexport function provideBlockTypeGuid(blockTypeGuid: string): void {\r\n provide(blockTypeGuidSymbol, blockTypeGuid);\r\n}\r\n\r\n/**\r\n * Gets the block type unique identifier of the current block in this component\r\n * chain.\r\n *\r\n * @returns The unique identifier of the block type.\r\n */\r\nexport function useBlockTypeGuid(): Guid | undefined {\r\n return inject(blockTypeGuidSymbol);\r\n}\r\n\r\n// #endregion\r\n\r\n// #region Person Preferences\r\n\r\nconst blockPreferenceProviderSymbol = Symbol();\r\n\r\n/** An no-op implementation of {@link IPersonPreferenceCollection}. */\r\nconst emptyPreferences: IPersonPreferenceCollection = {\r\n getValue(): string {\r\n return \"\";\r\n },\r\n setValue(): void {\r\n // Intentionally empty.\r\n },\r\n getKeys(): string[] {\r\n return [];\r\n },\r\n containsKey(): boolean {\r\n return false;\r\n },\r\n save(): Promise {\r\n return Promise.resolve();\r\n },\r\n withPrefix(): IPersonPreferenceCollection {\r\n return emptyPreferences;\r\n },\r\n on(): void {\r\n // Intentionally empty.\r\n },\r\n off(): void {\r\n // Intentionally empty.\r\n }\r\n};\r\n\r\nconst emptyPreferenceProvider: IBlockPersonPreferencesProvider = {\r\n blockPreferences: emptyPreferences,\r\n getGlobalPreferences() {\r\n return Promise.resolve(emptyPreferences);\r\n },\r\n getEntityPreferences() {\r\n return Promise.resolve(emptyPreferences);\r\n }\r\n};\r\n\r\n/**\r\n * Provides the person preferences provider that will be used by components\r\n * to access the person preferences associated with their block.\r\n *\r\n * @private This is an internal method and should not be used by plugins.\r\n *\r\n * @param blockGuid The unique identifier of the block.\r\n */\r\nexport function providePersonPreferences(provider: IBlockPersonPreferencesProvider): void {\r\n provide(blockPreferenceProviderSymbol, provider);\r\n}\r\n\r\n/**\r\n * Gets the person preference provider that can be used to access block\r\n * preferences as well as other preferences.\r\n *\r\n * @returns An object that implements {@link IBlockPersonPreferencesProvider}.\r\n */\r\nexport function usePersonPreferences(): IBlockPersonPreferencesProvider {\r\n return inject(blockPreferenceProviderSymbol)\r\n ?? emptyPreferenceProvider;\r\n}\r\n\r\n// #endregion\r\n","// \r\n// Copyright by the Spark Development Network\r\n//\r\n// Licensed under the Rock Community License (the \"License\");\r\n// you may not use this file except in compliance with the License.\r\n// You may obtain a copy of the License at\r\n//\r\n// http://www.rockrms.com/license\r\n//\r\n// Unless required by applicable law or agreed to in writing, software\r\n// distributed under the License is distributed on an \"AS IS\" BASIS,\r\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n// See the License for the specific language governing permissions and\r\n// limitations under the License.\r\n// \r\n//\r\n\r\n/**\r\n * Transform the value into true, false, or null\r\n * @param val\r\n */\r\nexport function asBooleanOrNull(val: unknown): boolean | null {\r\n if (val === undefined || val === null) {\r\n return null;\r\n }\r\n\r\n if (typeof val === \"boolean\") {\r\n return val;\r\n }\r\n\r\n if (typeof val === \"string\") {\r\n const asString = (val || \"\").trim().toLowerCase();\r\n\r\n if (!asString) {\r\n return null;\r\n }\r\n\r\n return [\"true\", \"yes\", \"t\", \"y\", \"1\"].indexOf(asString) !== -1;\r\n }\r\n\r\n if (typeof val === \"number\") {\r\n return !!val;\r\n }\r\n\r\n return null;\r\n}\r\n\r\n/**\r\n * Transform the value into true or false\r\n * @param val\r\n */\r\nexport function asBoolean(val: unknown): boolean {\r\n return !!asBooleanOrNull(val);\r\n}\r\n\r\n/** Transform the value into the strings \"Yes\", \"No\", or null */\r\nexport function asYesNoOrNull(val: unknown): \"Yes\" | \"No\" | null {\r\n const boolOrNull = asBooleanOrNull(val);\r\n\r\n if (boolOrNull === null) {\r\n return null;\r\n }\r\n\r\n return boolOrNull ? \"Yes\" : \"No\";\r\n}\r\n\r\n/** Transform the value into the strings \"True\", \"False\", or null */\r\nexport function asTrueFalseOrNull(val: unknown): \"True\" | \"False\" | null {\r\n const boolOrNull = asBooleanOrNull(val);\r\n\r\n if (boolOrNull === null) {\r\n return null;\r\n }\r\n\r\n return boolOrNull ? \"True\" : \"False\";\r\n}\r\n\r\n/** Transform the value into the strings \"True\" if truthy or \"False\" if falsey */\r\nexport function asTrueOrFalseString(val: unknown): \"True\" | \"False\" {\r\n const boolOrNull = asBooleanOrNull(val);\r\n\r\n return boolOrNull ? \"True\" : \"False\";\r\n}\r\n","// \r\n// Copyright by the Spark Development Network\r\n//\r\n// Licensed under the Rock Community License (the \"License\");\r\n// you may not use this file except in compliance with the License.\r\n// You may obtain a copy of the License at\r\n//\r\n// http://www.rockrms.com/license\r\n//\r\n// Unless required by applicable law or agreed to in writing, software\r\n// distributed under the License is distributed on an \"AS IS\" BASIS,\r\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n// See the License for the specific language governing permissions and\r\n// limitations under the License.\r\n// \r\n\r\nimport { RockDateTime } from \"./rockDateTime\";\r\n\r\n\r\ntype CacheEntry = {\r\n value: T;\r\n expiration: number;\r\n};\r\n\r\nconst keyPrefix = \"ObsidianCache_\";\r\n\r\n/**\r\n* Stores the value using the given key. The cache will expire at the expiration or in\r\n* 1 minute if none is provided\r\n* @param key Unique key to store the value under\r\n* @param value Value to store\r\n* @param ttl Time to live in seconds (how long the cached value is valid for). Defaults to 60 if not provided.\r\n*/\r\nfunction set(key: string, value: T, ttl: number = 60): void {\r\n // Can be removed in Rock v18\r\n if (typeof ttl !== \"number\") {\r\n ttl = 60;\r\n console.warn(\"Cache.set has been changed to use a Time-To-Live instead of an expiration date/time. Please update your code to use a Time-To-Live.\");\r\n }\r\n\r\n const expiration = RockDateTime.now().addSeconds(ttl).toMilliseconds();\r\n\r\n const cache: CacheEntry = { expiration, value };\r\n const cacheJson = JSON.stringify(cache);\r\n sessionStorage.setItem(keyPrefix + key, cacheJson);\r\n}\r\n\r\n/**\r\n * Gets a stored cache value if there is one that has not yet expired.\r\n * @param key\r\n */\r\nfunction get(key: string): T | null {\r\n const cacheJson = sessionStorage.getItem(keyPrefix + key);\r\n\r\n if (!cacheJson) {\r\n return null;\r\n }\r\n\r\n const cache = JSON.parse(cacheJson) as CacheEntry;\r\n\r\n if (!cache || !cache.expiration) {\r\n return null;\r\n }\r\n\r\n if (cache.expiration < RockDateTime.now().toMilliseconds()) {\r\n return null;\r\n }\r\n\r\n return cache.value;\r\n}\r\n\r\n// Stores promises that are in flight so that if a second fetch is fired before the first resolves\r\n// (before value gets cached), just return the promise that's already in flight to prevent duplicate calls\r\nconst promiseCache: Record | undefined> = {};\r\n\r\n/**\r\n * Since Promises can't be cached, we need to store them in memory until we get the result back. This wraps\r\n * a function in another function that returns a promise and...\r\n * - If there's a cached result, return it\r\n * - Otherwise if there's a cached Promise, return it\r\n * - Otherwise call the given function and cache it's promise and return it. Once the the Promise resolves, cache its result\r\n *\r\n * @param key Key for identifying the cached values\r\n * @param fn Function that returns a Promise that we want to cache the value of\r\n * @param ttl Time to live in seconds (how long the cached value is valid for)\r\n *\r\n * @returns A function that wraps the given function, that can be called instead of the given function and that will use the cache when appropriate.\r\n */\r\nfunction cachePromiseFactory(key: string, fn: () => Promise, ttl?: number): () => Promise {\r\n return async function (): Promise {\r\n // Can be removed in Rock v18\r\n if (typeof ttl !== \"number\" || ttl !== undefined) {\r\n console.warn(\"Cache.cachePromiseFactory has been changed to use a Time-To-Live instead of an expiration date/time. Please update your code to use a Time-To-Live.\");\r\n }\r\n\r\n // If it's cached, grab it\r\n const cachedResult = get(key);\r\n if (cachedResult) {\r\n return cachedResult;\r\n }\r\n\r\n // If it's not cached yet but we've already started fetching it\r\n // (it's not cached until we receive the results), return the existing Promise\r\n if (promiseCache[key]) {\r\n return promiseCache[key] as Promise;\r\n }\r\n\r\n // Not stored anywhere, so fetch it and save it on the stored Promise for the next call\r\n promiseCache[key] = fn();\r\n\r\n // Once it's resolved, cache the result\r\n promiseCache[key]?.then((result) => {\r\n set(key, result, ttl);\r\n delete promiseCache[key];\r\n return result;\r\n }).catch((e: Error) => {\r\n // Something's wrong, let's get rid of the stored promise, so we can try again.\r\n delete promiseCache[key];\r\n throw e;\r\n });\r\n\r\n return promiseCache[key] as Promise;\r\n };\r\n}\r\n\r\n\r\nexport default {\r\n set,\r\n get,\r\n cachePromiseFactory\r\n};\r\n","// \r\n// Copyright by the Spark Development Network\r\n//\r\n// Licensed under the Rock Community License (the \"License\");\r\n// you may not use this file except in compliance with the License.\r\n// You may obtain a copy of the License at\r\n//\r\n// http://www.rockrms.com/license\r\n//\r\n// Unless required by applicable law or agreed to in writing, software\r\n// distributed under the License is distributed on an \"AS IS\" BASIS,\r\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n// See the License for the specific language governing permissions and\r\n// limitations under the License.\r\n// \r\n//\r\n\r\nimport { Guid } from \"@Obsidian/Types\";\r\nimport { newGuid } from \"./guid\";\r\nimport { inject, nextTick, provide } from \"vue\";\r\n\r\nconst suspenseSymbol = Symbol(\"RockSuspense\");\r\n\r\n/**\r\n * Defines the interface for a provider of suspense monitoring. These are used\r\n * to track asynchronous operations that components may be performing so the\r\n * watching component can perform an operation once all pending operations\r\n * have completed.\r\n */\r\nexport interface ISuspenseProvider {\r\n /**\r\n * Adds a new operation identified by the promise. When the promise\r\n * either resolves or fails the operation is considered completed.\r\n *\r\n * @param operation The promise that represents the operation.\r\n */\r\n addOperation(operation: Promise): void;\r\n\r\n /**\r\n * Notes that an asynchronous operation has started on a child component.\r\n *\r\n * @param key The key that identifies the operation.\r\n */\r\n startAsyncOperation(key: Guid): void;\r\n\r\n /**\r\n * Notes that an asynchrounous operation has completed on a child component.\r\n *\r\n * @param key The key that was previously passed to startAsyncOperation.\r\n */\r\n completeAsyncOperation(key: Guid): void;\r\n}\r\n\r\n/**\r\n * A basic provider that handles the guts of a suspense provider. This can be\r\n * used by components that need to know when child components have completed\r\n * their work.\r\n */\r\nexport class BasicSuspenseProvider implements ISuspenseProvider {\r\n private readonly operationKey: Guid;\r\n\r\n private readonly parentProvider: ISuspenseProvider | undefined;\r\n\r\n private readonly pendingOperations: Guid[];\r\n\r\n private finishedHandlers: (() => void)[];\r\n\r\n /**\r\n * Creates a new suspense provider.\r\n *\r\n * @param parentProvider The parent suspense provider that will be notified of pending operations.\r\n */\r\n constructor(parentProvider: ISuspenseProvider | undefined) {\r\n this.operationKey = newGuid();\r\n this.parentProvider = parentProvider;\r\n this.pendingOperations = [];\r\n this.finishedHandlers = [];\r\n }\r\n\r\n /**\r\n * Called when all pending operations are complete. Notifies all handlers\r\n * that the pending operations have completed as well as the parent provider.\r\n */\r\n private allOperationsComplete(): void {\r\n // Wait until the next Vue tick in case a new async operation started.\r\n // This can happen, for example, with defineAsyncComponent(). It will\r\n // complete its async operation (loading the JS file) and then the\r\n // component defined in the file might start an async operation. This\r\n // prevents us from completing too soon.\r\n nextTick(() => {\r\n // Verify nothing started a new asynchronous operation while we\r\n // we waiting for the next tick.\r\n if (this.pendingOperations.length !== 0) {\r\n return;\r\n }\r\n\r\n // Notify all pending handlers that all operations completed.\r\n for (const handler of this.finishedHandlers) {\r\n handler();\r\n }\r\n this.finishedHandlers = [];\r\n\r\n // Notify the parent that our own pending operation has completed.\r\n if (this.parentProvider) {\r\n this.parentProvider.completeAsyncOperation(this.operationKey);\r\n }\r\n });\r\n }\r\n\r\n /**\r\n * Adds a new operation identified by the promise. When the promise\r\n * either resolves or fails the operation is considered completed.\r\n *\r\n * @param operation The promise that represents the operation.\r\n */\r\n public addOperation(operation: Promise): void {\r\n const operationKey = newGuid();\r\n\r\n this.startAsyncOperation(operationKey);\r\n\r\n operation.then(() => this.completeAsyncOperation(operationKey))\r\n .catch(() => this.completeAsyncOperation(operationKey));\r\n }\r\n\r\n /**\r\n * Notes that an asynchronous operation has started on a child component.\r\n *\r\n * @param key The key that identifies the operation.\r\n */\r\n public startAsyncOperation(key: Guid): void {\r\n this.pendingOperations.push(key);\r\n\r\n // If this is the first operation we started, notify the parent provider.\r\n if (this.pendingOperations.length === 1 && this.parentProvider) {\r\n this.parentProvider.startAsyncOperation(this.operationKey);\r\n }\r\n }\r\n\r\n /**\r\n * Notes that an asynchrounous operation has completed on a child component.\r\n *\r\n * @param key The key that was previously passed to startAsyncOperation.\r\n */\r\n public completeAsyncOperation(key: Guid): void {\r\n const index = this.pendingOperations.indexOf(key);\r\n\r\n if (index !== -1) {\r\n this.pendingOperations.splice(index, 1);\r\n }\r\n\r\n // If this was the last operation then send notifications.\r\n if (this.pendingOperations.length === 0) {\r\n this.allOperationsComplete();\r\n }\r\n }\r\n\r\n /**\r\n * Checks if this provider has any asynchronous operations that are still\r\n * pending completion.\r\n *\r\n * @returns true if there are pending operations; otherwise false.\r\n */\r\n public hasPendingOperations(): boolean {\r\n return this.pendingOperations.length > 0;\r\n }\r\n\r\n /**\r\n * Adds a new handler that is called when all pending operations have been\r\n * completed. This is a fire-once, meaning the callback will only be called\r\n * when the current pending operations have completed. If new operations\r\n * begin after the callback is executed it will not be called again unless\r\n * it is added with this method again.\r\n *\r\n * @param callback The function to call when all pending operations have completed.\r\n */\r\n public addFinishedHandler(callback: () => void): void {\r\n this.finishedHandlers.push(callback);\r\n }\r\n}\r\n\r\n/**\r\n * Provides a new suspense provider to any child components.\r\n *\r\n * @param provider The provider to make available to child components.\r\n */\r\nexport function provideSuspense(provider: ISuspenseProvider): void {\r\n provide(suspenseSymbol, provider);\r\n}\r\n\r\n/**\r\n * Uses the current suspense provider that was defined by any parent component.\r\n *\r\n * @returns The suspense provider if one was defined; otherwise undefined.\r\n */\r\nexport function useSuspense(): ISuspenseProvider | undefined {\r\n return inject(suspenseSymbol, undefined);\r\n}\r\n\r\n","// \r\n// Copyright by the Spark Development Network\r\n//\r\n// Licensed under the Rock Community License (the \"License\");\r\n// you may not use this file except in compliance with the License.\r\n// You may obtain a copy of the License at\r\n//\r\n// http://www.rockrms.com/license\r\n//\r\n// Unless required by applicable law or agreed to in writing, software\r\n// distributed under the License is distributed on an \"AS IS\" BASIS,\r\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n// See the License for the specific language governing permissions and\r\n// limitations under the License.\r\n// \r\n\r\nimport { CurrencyInfoBag } from \"@Obsidian/ViewModels/Rest/Utilities/currencyInfoBag\";\r\n\r\n// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat\r\n// Number.toLocaleString takes the same options as Intl.NumberFormat\r\n// Most of the options probably won't get used, so just add the ones you need to use to this when needed\r\ntype NumberFormatOptions = {\r\n useGrouping?: boolean // MDN gives other possible values, but TS is complaining that it should only be boolean\r\n};\r\n\r\n/**\r\n * Get a formatted string.\r\n * Ex: 10001.2 => 10,001.2\r\n * @param num\r\n */\r\nexport function asFormattedString(num: number | null, digits?: number, options: NumberFormatOptions = {}): string {\r\n if (num === null) {\r\n return \"\";\r\n }\r\n\r\n return num.toLocaleString(\r\n \"en-US\",\r\n {\r\n minimumFractionDigits: digits,\r\n maximumFractionDigits: digits ?? 9,\r\n ...options\r\n }\r\n );\r\n}\r\n\r\n/**\r\n * Get a number value from a formatted string. If the number cannot be parsed, then zero is returned by default.\r\n * Ex: $1,000.20 => 1000.2\r\n * @param str\r\n */\r\nexport function toNumber(str?: string | number | null): number {\r\n return toNumberOrNull(str) || 0;\r\n}\r\n\r\n/**\r\n * Get a number value from a formatted string. If the number cannot be parsed, then null is returned by default.\r\n * Ex: $1,000.20 => 1000.2\r\n * @param str\r\n */\r\nexport function toNumberOrNull(str?: string | number | null): number | null {\r\n if (str === null || str === undefined || str === \"\") {\r\n return null;\r\n }\r\n\r\n if (typeof str === \"number\") {\r\n return str;\r\n }\r\n\r\n const replaced = str.replace(/[$,]/g, \"\");\r\n const num = Number(replaced);\r\n\r\n return !isNaN(num) ? num : null;\r\n}\r\n\r\n/**\r\n * Get a currency value from a string or number. If the number cannot be parsed, then null is returned by default.\r\n * Ex: 1000.20 => $1,000.20\r\n * @param value The value to be converted to a currency.\r\n */\r\nexport function toCurrencyOrNull(value?: string | number | null, currencyInfo: CurrencyInfoBag | null = null): string | null {\r\n if (typeof value === \"string\") {\r\n value = toNumberOrNull(value);\r\n }\r\n\r\n if (value === null || value === undefined) {\r\n return null;\r\n }\r\n const currencySymbol = currencyInfo?.symbol ?? \"$\";\r\n const currencyDecimalPlaces = currencyInfo?.decimalPlaces ?? 2;\r\n return `${currencySymbol}${asFormattedString(value, currencyDecimalPlaces)}`;\r\n}\r\n\r\n/**\r\n * Adds an ordinal suffix.\r\n * Ex: 1 => 1st\r\n * @param num\r\n */\r\nexport function toOrdinalSuffix(num?: number | null): string {\r\n if (!num) {\r\n return \"\";\r\n }\r\n\r\n const j = num % 10;\r\n const k = num % 100;\r\n\r\n if (j == 1 && k != 11) {\r\n return num + \"st\";\r\n }\r\n if (j == 2 && k != 12) {\r\n return num + \"nd\";\r\n }\r\n if (j == 3 && k != 13) {\r\n return num + \"rd\";\r\n }\r\n return num + \"th\";\r\n}\r\n\r\n/**\r\n * Convert a number to an ordinal.\r\n * Ex: 1 => first, 10 => tenth\r\n *\r\n * Anything larger than 10 will be converted to the number with an ordinal suffix.\r\n * Ex: 123 => 123rd, 1000 => 1000th\r\n * @param num\r\n */\r\nexport function toOrdinal(num?: number | null): string {\r\n if (!num) {\r\n return \"\";\r\n }\r\n\r\n switch (num) {\r\n case 1: return \"first\";\r\n case 2: return \"second\";\r\n case 3: return \"third\";\r\n case 4: return \"fourth\";\r\n case 5: return \"fifth\";\r\n case 6: return \"sixth\";\r\n case 7: return \"seventh\";\r\n case 8: return \"eighth\";\r\n case 9: return \"ninth\";\r\n case 10: return \"tenth\";\r\n default: return toOrdinalSuffix(num);\r\n }\r\n}\r\n\r\n/**\r\n * Convert a number to a word.\r\n * Ex: 1 => \"one\", 10 => \"ten\"\r\n *\r\n * Anything larger than 10 will be returned as a number string instead of a word.\r\n * Ex: 123 => \"123\", 1000 => \"1000\"\r\n * @param num\r\n */\r\nexport function toWord(num?: number | null): string {\r\n if (num === null || num === undefined) {\r\n return \"\";\r\n }\r\n\r\n switch (num) {\r\n case 1: return \"one\";\r\n case 2: return \"two\";\r\n case 3: return \"three\";\r\n case 4: return \"four\";\r\n case 5: return \"five\";\r\n case 6: return \"six\";\r\n case 7: return \"seven\";\r\n case 8: return \"eight\";\r\n case 9: return \"nine\";\r\n case 10: return \"ten\";\r\n default: return `${num}`;\r\n }\r\n}\r\n\r\nexport function zeroPad(num: number, length: number): string {\r\n let str = num.toString();\r\n\r\n while (str.length < length) {\r\n str = \"0\" + str;\r\n }\r\n\r\n return str;\r\n}\r\n\r\nexport function toDecimalPlaces(num: number, decimalPlaces: number): number {\r\n decimalPlaces = Math.floor(decimalPlaces); // ensure it's an integer\r\n\r\n return Math.round(num * 10 ** decimalPlaces) / 10 ** decimalPlaces;\r\n}\r\n\r\n/**\r\n * Returns the string representation of an integer.\r\n * Ex: 1 => \"1\", 123456 => \"one hundred twenty-three thousand four hundred fifty-six\"\r\n *\r\n * Not reliable for numbers in the quadrillions and greater.\r\n *\r\n * @example\r\n * numberToWord(1) // one\r\n * numberToWord(2) // two\r\n * numberToWord(123456) // one hundred twenty-three thousand four hundred fifty-six\r\n * @param numb The number for which to get the string representation.\r\n * @returns \"one\", \"two\", ..., \"one thousand\", ..., (up to the max number allowed for JS).\r\n */\r\nexport function toWordFull(numb: number): string {\r\n const numberWords = {\r\n 0: \"zero\",\r\n 1: \"one\",\r\n 2: \"two\",\r\n 3: \"three\",\r\n 4: \"four\",\r\n 5: \"five\",\r\n 6: \"six\",\r\n 7: \"seven\",\r\n 8: \"eight\",\r\n 9: \"nine\",\r\n 10: \"ten\",\r\n 11: \"eleven\",\r\n 12: \"twelve\",\r\n 13: \"thirteen\",\r\n 14: \"fourteen\",\r\n 15: \"fifteen\",\r\n 16: \"sixteen\",\r\n 17: \"seventeen\",\r\n 18: \"eighteen\",\r\n 19: \"nineteen\",\r\n 20: \"twenty\",\r\n 30: \"thirty\",\r\n 40: \"forty\",\r\n 50: \"fifty\",\r\n 60: \"sixty\",\r\n 70: \"seventy\",\r\n 80: \"eighty\",\r\n 90: \"ninety\",\r\n 100: \"one hundred\",\r\n 1000: \"one thousand\",\r\n 1000000: \"one million\",\r\n 1000000000: \"one billion\",\r\n 1000000000000: \"one trillion\",\r\n 1000000000000000: \"one quadrillion\"\r\n };\r\n\r\n // Store constants for these since it is hard to distinguish between them at larger numbers.\r\n const oneHundred = 100;\r\n const oneThousand = 1000;\r\n const oneMillion = 1000000;\r\n const oneBillion = 1000000000;\r\n const oneTrillion = 1000000000000;\r\n const oneQuadrillion = 1000000000000000;\r\n\r\n if (numberWords[numb]) {\r\n return numberWords[numb];\r\n }\r\n\r\n function quadrillionsToWord(numb: number): string {\r\n const trillions = trillionsToWord(numb);\r\n if (numb >= oneQuadrillion) {\r\n const quadrillions = hundredsToWord(Number(numb.toString().slice(-18, -15)));\r\n if (trillions) {\r\n return `${quadrillions} quadrillion ${trillions}`;\r\n }\r\n else {\r\n return `${quadrillions} quadrillion`;\r\n }\r\n }\r\n else {\r\n return trillions;\r\n }\r\n }\r\n\r\n function trillionsToWord(numb: number): string {\r\n numb = Number(numb.toString().slice(-15));\r\n const billions = billionsToWord(numb);\r\n if (numb >= oneTrillion) {\r\n const trillions = hundredsToWord(Number(numb.toString().slice(-15, -12)));\r\n if (billions) {\r\n return `${trillions} trillion ${billions}`;\r\n }\r\n else {\r\n return `${trillions} trillion`;\r\n }\r\n }\r\n else {\r\n return billions;\r\n }\r\n }\r\n\r\n function billionsToWord(numb: number): string {\r\n numb = Number(numb.toString().slice(-12));\r\n const millions = millionsToWord(numb);\r\n if (numb >= oneBillion) {\r\n const billions = hundredsToWord(Number(numb.toString().slice(-12, -9)));\r\n if (millions) {\r\n return `${billions} billion ${millions}`;\r\n }\r\n else {\r\n return `${billions} billion`;\r\n }\r\n }\r\n else {\r\n return millions;\r\n }\r\n }\r\n\r\n function millionsToWord(numb: number): string {\r\n numb = Number(numb.toString().slice(-9));\r\n const thousands = thousandsToWord(numb);\r\n if (numb >= oneMillion) {\r\n const millions = hundredsToWord(Number(numb.toString().slice(-9, -6)));\r\n if (thousands) {\r\n return `${millions} million ${thousands}`;\r\n }\r\n else {\r\n return `${millions} million`;\r\n }\r\n }\r\n else {\r\n return thousands;\r\n }\r\n }\r\n\r\n function thousandsToWord(numb: number): string {\r\n numb = Number(numb.toString().slice(-6));\r\n const hundreds = hundredsToWord(numb);\r\n if (numb >= oneThousand) {\r\n const thousands = hundredsToWord(Number(numb.toString().slice(-6, -3)));\r\n if (hundreds) {\r\n return `${thousands} thousand ${hundreds}`;\r\n }\r\n else {\r\n return `${thousands} thousandths`;\r\n }\r\n }\r\n else {\r\n return hundreds;\r\n }\r\n }\r\n\r\n function hundredsToWord(numb: number): string {\r\n numb = Number(numb.toString().slice(-3));\r\n\r\n if (numberWords[numb]) {\r\n return numberWords[numb];\r\n }\r\n\r\n const tens = tensToWord(numb);\r\n\r\n if (numb >= oneHundred) {\r\n const hundreds = Number(numb.toString().slice(-3, -2));\r\n if (tens) {\r\n return `${numberWords[hundreds]} hundred ${tens}`;\r\n }\r\n else {\r\n return `${numberWords[hundreds]} hundred`;\r\n }\r\n }\r\n else {\r\n return tens;\r\n }\r\n }\r\n\r\n function tensToWord(numb: number): string {\r\n numb = Number(numb.toString().slice(-2));\r\n\r\n if (numberWords[numb]) {\r\n return numberWords[numb];\r\n }\r\n\r\n const ones = onesToWord(numb);\r\n\r\n if (numb >= 20) {\r\n const tens = Number(numb.toString().slice(-2, -1));\r\n\r\n if (ones) {\r\n return `${numberWords[tens * 10]}-${ones}`;\r\n }\r\n else {\r\n return numberWords[tens * 10];\r\n }\r\n }\r\n else {\r\n return ones;\r\n }\r\n }\r\n\r\n function onesToWord(numb: number): string {\r\n numb = Number(numb.toString().slice(-1));\r\n return numberWords[numb];\r\n }\r\n\r\n return quadrillionsToWord(numb);\r\n}\r\n\r\nexport default {\r\n toOrdinal,\r\n toOrdinalSuffix,\r\n toNumberOrNull,\r\n asFormattedString\r\n};\r\n","// \r\n// Copyright by the Spark Development Network\r\n//\r\n// Licensed under the Rock Community License (the \"License\");\r\n// you may not use this file except in compliance with the License.\r\n// You may obtain a copy of the License at\r\n//\r\n// http://www.rockrms.com/license\r\n//\r\n// Unless required by applicable law or agreed to in writing, software\r\n// distributed under the License is distributed on an \"AS IS\" BASIS,\r\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n// See the License for the specific language governing permissions and\r\n// limitations under the License.\r\n// \r\n\r\nimport { AsyncComponentLoader, Component, ComponentPublicInstance, defineAsyncComponent as vueDefineAsyncComponent, ExtractPropTypes, PropType, reactive, ref, Ref, VNode, watch, WatchOptions, render, createVNode } from \"vue\";\r\nimport { deepEqual } from \"./util\";\r\nimport { useSuspense } from \"./suspense\";\r\nimport { newGuid } from \"./guid\";\r\nimport { ControlLazyMode } from \"@Obsidian/Enums/Controls/controlLazyMode\";\r\nimport { PickerDisplayStyle } from \"@Obsidian/Enums/Controls/pickerDisplayStyle\";\r\nimport { FilterMode } from \"@Obsidian/Enums/Reporting/filterMode\";\r\nimport { ExtendedRef, ExtendedRefContext } from \"@Obsidian/Types/Utility/component\";\r\nimport type { RulesPropType, ValidationRule } from \"@Obsidian/Types/validationRules\";\r\nimport { toNumberOrNull } from \"./numberUtils\";\r\n\r\ntype Prop = { [key: string]: unknown };\r\ntype PropKey = Extract;\r\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\r\ntype EmitFn = E extends Array ? (event: EE, ...args: any[]) => void : (event: E, ...args: any[]) => void;\r\n\r\n/**\r\n * Utility function for when you are using a component that takes a v-model\r\n * and uses that model as a v-model in that component's template. It creates\r\n * a new ref that keeps itself up-to-date with the given model and fires an\r\n * 'update:MODELNAME' event when it gets changed.\r\n *\r\n * Ensure the related `props` and `emits` are specified to ensure there are\r\n * no type issues.\r\n */\r\nexport function useVModelPassthrough, E extends `update:${K}`>(props: T, modelName: K, emit: EmitFn, options?: WatchOptions): Ref {\r\n const internalValue = ref(props[modelName]) as Ref;\r\n\r\n watch(() => props[modelName], val => updateRefValue(internalValue, val), options);\r\n watch(internalValue, val => {\r\n if (val !== props[modelName]) {\r\n emit(`update:${modelName}`, val);\r\n }\r\n }, options);\r\n\r\n return internalValue;\r\n}\r\n\r\n/**\r\n * Utility function for when you are using a component that takes a v-model\r\n * and uses that model as a v-model in that component's template. It creates\r\n * a new ref that keeps itself up-to-date with the given model and fires an\r\n * 'update:MODELNAME' event when it gets changed. It also gives a means of watching\r\n * the model prop for any changes (verifies that the prop change is different than\r\n * the current value first)\r\n *\r\n * Ensure the related `props` and `emits` are specified to ensure there are\r\n * no type issues.\r\n */\r\nexport function useVModelPassthroughWithPropUpdateCheck, E extends `update:${K}`>(props: T, modelName: K, emit: EmitFn, options?: WatchOptions): [Ref, (fn: () => unknown) => void] {\r\n const internalValue = ref(props[modelName]) as Ref;\r\n const listeners: (() => void)[] = [];\r\n\r\n watch(() => props[modelName], val => {\r\n if (updateRefValue(internalValue, val)) {\r\n onPropUpdate();\r\n }\r\n }, options);\r\n watch(internalValue, val => emit(`update:${modelName}`, val), options);\r\n\r\n function onPropUpdate(): void {\r\n listeners.forEach(fn => fn());\r\n }\r\n\r\n function addPropUpdateListener(fn: () => unknown): void {\r\n listeners.push(fn);\r\n }\r\n\r\n return [internalValue, addPropUpdateListener];\r\n}\r\n\r\n/**\r\n * Updates the Ref value, but only if the new value is actually different than\r\n * the current value. A deep comparison is performed.\r\n *\r\n * @param target The target Ref object to be updated.\r\n * @param value The new value to be assigned to the target.\r\n *\r\n * @returns True if the target was updated, otherwise false.\r\n */\r\nexport function updateRefValue(target: Ref, value: TV): boolean {\r\n if (deepEqual(target.value, value, true)) {\r\n return false;\r\n }\r\n\r\n target.value = value;\r\n\r\n return true;\r\n}\r\n\r\n/**\r\n * Defines a component that will be loaded asynchronously. This contains logic\r\n * to properly work with the RockSuspense control.\r\n *\r\n * @param source The function to call to load the component.\r\n *\r\n * @returns The component that was loaded.\r\n */\r\nexport function defineAsyncComponent(source: AsyncComponentLoader): T {\r\n return vueDefineAsyncComponent(async () => {\r\n const suspense = useSuspense();\r\n const operationKey = newGuid();\r\n\r\n suspense?.startAsyncOperation(operationKey);\r\n const component = await source();\r\n suspense?.completeAsyncOperation(operationKey);\r\n\r\n return component;\r\n });\r\n}\r\n\r\n// #region Standard Form Field\r\n\r\ntype StandardRockFormFieldProps = {\r\n label: {\r\n type: PropType,\r\n default: \"\"\r\n },\r\n\r\n disableLabel: {\r\n type: PropType,\r\n default: false\r\n },\r\n\r\n help: {\r\n type: PropType,\r\n default: \"\"\r\n },\r\n\r\n rules: RulesPropType,\r\n\r\n formGroupClasses: {\r\n type: PropType,\r\n default: \"\"\r\n },\r\n\r\n validationTitle: {\r\n type: PropType,\r\n default: \"\"\r\n },\r\n\r\n isRequiredIndicatorHidden: {\r\n type: PropType,\r\n default: false\r\n }\r\n};\r\n\r\n/** The standard component props that should be included when using RockFormField. */\r\nexport const standardRockFormFieldProps: StandardRockFormFieldProps = {\r\n label: {\r\n type: String as PropType,\r\n default: \"\"\r\n },\r\n\r\n disableLabel: {\r\n type: Boolean,\r\n default: false\r\n },\r\n\r\n help: {\r\n type: String as PropType,\r\n default: \"\"\r\n },\r\n\r\n rules: {\r\n type: [Array, Object, String] as PropType,\r\n default: \"\"\r\n },\r\n\r\n formGroupClasses: {\r\n type: String as PropType,\r\n default: \"\"\r\n },\r\n\r\n validationTitle: {\r\n type: String as PropType,\r\n default: \"\"\r\n },\r\n\r\n isRequiredIndicatorHidden: {\r\n type: Boolean as PropType,\r\n default: false\r\n }\r\n};\r\n\r\n/**\r\n * Copies the known properties for the standard rock form field props from\r\n * the source object to the destination object.\r\n *\r\n * @param source The source object to copy the values from.\r\n * @param destination The destination object to copy the values to.\r\n */\r\nfunction copyStandardRockFormFieldProps(source: ExtractPropTypes, destination: ExtractPropTypes): void {\r\n destination.formGroupClasses = source.formGroupClasses;\r\n destination.help = source.help;\r\n destination.label = source.label;\r\n destination.rules = source.rules;\r\n destination.validationTitle = source.validationTitle;\r\n}\r\n\r\n/**\r\n * Configures the basic properties that should be passed to the RockFormField\r\n * component. The value returned by this function should be used with v-bind on\r\n * the RockFormField in order to pass all the defined prop values to it.\r\n *\r\n * @param props The props of the component that will be using the RockFormField.\r\n *\r\n * @returns An object of prop values that can be used with v-bind.\r\n */\r\nexport function useStandardRockFormFieldProps(props: ExtractPropTypes]