{"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/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/slidingDateRange.ts","../../Framework/Utility/structuredContentEditor.ts","../../Framework/Utility/tooltip.ts","../../Framework/Utility/treeItemProviders.ts","../../Framework/Utility/url.ts","../../Framework/Utility/validationRules.ts"],"sourcesContent":["// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n//\n\nimport { Guid } from \"@Obsidian/Types\";\nimport axios, { AxiosResponse } from \"axios\";\nimport { ListItemBag } from \"@Obsidian/ViewModels/Utility/listItemBag\";\nimport { HttpBodyData, HttpMethod, HttpFunctions, HttpResult, HttpUrlParams } from \"@Obsidian/Types/Utility/http\";\nimport { inject, provide, getCurrentInstance } from \"vue\";\n\n\n// #region HTTP Requests\n\n/**\n * Make an API call. This is only place Axios (or AJAX library) should be referenced to allow tools like performance metrics to provide\n * better insights.\n * @param method\n * @param url\n * @param params\n * @param data\n */\nasync function doApiCallRaw(method: HttpMethod, url: string, params: HttpUrlParams, data: HttpBodyData): Promise> {\n return await axios({\n method,\n url,\n params,\n data\n });\n}\n\n/**\n * Make an API call. This is a special use function that should not\n * normally be used. Instead call useHttp() to get the HTTP functions that\n * can be used.\n *\n * @param {string} method The HTTP method, such as GET\n * @param {string} url The endpoint to access, such as /api/campuses/\n * @param {object} params Query parameter object. Will be converted to ?key1=value1&key2=value2 as part of the URL.\n * @param {any} data This will be the body of the request\n */\nexport async function doApiCall(method: HttpMethod, url: string, params: HttpUrlParams = undefined, data: HttpBodyData = undefined): Promise> {\n try {\n const result = await doApiCallRaw(method, url, params, data);\n\n return {\n data: result.data as T,\n isError: false,\n isSuccess: true,\n statusCode: result.status,\n errorMessage: null\n } as HttpResult;\n }\n catch (e) {\n if (axios.isAxiosError(e)) {\n if (e.response?.data?.Message || e?.response?.data?.message) {\n return {\n data: null,\n isError: true,\n isSuccess: false,\n statusCode: e.response.status,\n errorMessage: e?.response?.data?.Message ?? e.response.data.message\n } as HttpResult;\n }\n\n return {\n data: null,\n isError: true,\n isSuccess: false,\n statusCode: e.response?.status ?? 0,\n errorMessage: null\n } as HttpResult;\n }\n else {\n return {\n data: null,\n isError: true,\n isSuccess: false,\n statusCode: 0,\n errorMessage: null\n } as HttpResult;\n }\n }\n}\n\n/**\n * Make a GET HTTP request. This is a special use function that should not\n * normally be used. Instead call useHttp() to get the HTTP functions that\n * can be used.\n *\n * @param {string} url The endpoint to access, such as /api/campuses/\n * @param {object} params Query parameter object. Will be converted to ?key1=value1&key2=value2 as part of the URL.\n */\nexport async function get(url: string, params: HttpUrlParams = undefined): Promise> {\n return await doApiCall(\"GET\", url, params, undefined);\n}\n\n/**\n * Make a POST HTTP request. This is a special use function that should not\n * normally be used. Instead call useHttp() to get the HTTP functions that\n * can be used.\n *\n * @param {string} url The endpoint to access, such as /api/campuses/\n * @param {object} params Query parameter object. Will be converted to ?key1=value1&key2=value2 as part of the URL.\n * @param {any} data This will be the body of the request\n */\nexport async function post(url: string, params: HttpUrlParams = undefined, data: HttpBodyData = undefined): Promise> {\n return await doApiCall(\"POST\", url, params, data);\n}\n\nconst httpFunctionsSymbol = Symbol(\"http-functions\");\n\n/**\n * Provides the HTTP functions that child components will use. This is an\n * internal API and should not be used by third party components.\n *\n * @param functions The functions that will be made available to child components.\n */\nexport function provideHttp(functions: HttpFunctions): void {\n provide(httpFunctionsSymbol, functions);\n}\n\n/**\n * Gets the HTTP functions that can be used by the component. This is the\n * standard way to make HTTP requests.\n *\n * @returns An object that contains the functions which can be called.\n */\nexport function useHttp(): HttpFunctions {\n let http: HttpFunctions | undefined;\n\n // Check if we are inside a setup instance. This prevents warnings\n // from being displayed if being called outside a setup() function.\n if (getCurrentInstance()) {\n http = inject(httpFunctionsSymbol);\n }\n\n return http || {\n doApiCall: doApiCall,\n get: get,\n post: post\n };\n}\n\n// #endregion\n\n// #region File Upload\n\ntype FileUploadResponse = {\n /* eslint-disable @typescript-eslint/naming-convention */\n Guid: Guid;\n FileName: string;\n /* eslint-enable */\n};\n\n/**\n * Progress reporting callback used when uploading a file into Rock.\n */\nexport type UploadProgressCallback = (progress: number, total: number, percent: number) => void;\n\n/**\n * Options used when uploading a file into Rock to change the default behavior.\n */\nexport type UploadOptions = {\n /**\n * The base URL to use when uploading the file, must accept the same parameters\n * and as the standard FileUploader.ashx handler.\n */\n baseUrl?: string;\n\n /** True if the file should be uploaded as temporary, only applies to binary files. */\n isTemporary?: boolean;\n\n /** A function to call to report the ongoing progress of the upload. */\n progress: UploadProgressCallback;\n};\n\n/**\n * Uploads a file in the form data into Rock. This is an internal function and\n * should not be exported.\n *\n * @param url The URL to use for the POST request.\n * @param data The form data to send in the request body.\n * @param progress The optional callback to use to report progress.\n *\n * @returns The response from the upload handler.\n */\nasync function uploadFile(url: string, data: FormData, progress: UploadProgressCallback | undefined): Promise {\n const result = await axios.post(url, data, {\n headers: {\n \"Content-Type\": \"multipart/form-data\"\n },\n onUploadProgress: (event: ProgressEvent) => {\n if (progress) {\n progress(event.loaded, event.total, Math.floor(event.loaded * 100 / event.total));\n }\n }\n });\n\n // Check for a \"everything went perfectly fine\" response.\n if (result.status === 200 && typeof result.data === \"object\") {\n return result.data;\n }\n\n if (result.status === 406) {\n throw \"File type is not allowed.\";\n }\n\n if (typeof result.data === \"string\") {\n throw result.data;\n }\n\n throw \"Upload failed.\";\n}\n\n/**\n * Uploads a file to the Rock file system, usually inside the ~/Content directory.\n *\n * @param file The file to be uploaded to the server.\n * @param encryptedRootFolder The encrypted root folder specified by the server,\n * this specifies the jail the upload operation is limited to.\n * @param folderPath The additional sub-folder path to use inside the root folder.\n * @param options The options to use when uploading the file.\n *\n * @returns A ListItemBag that contains the scrubbed filename that was uploaded.\n */\nexport async function uploadContentFile(file: File, encryptedRootFolder: string, folderPath: string, options?: UploadOptions): Promise {\n const url = `${options?.baseUrl ?? \"/FileUploader.ashx\"}?rootFolder=${encryptedRootFolder}`;\n const formData = new FormData();\n\n formData.append(\"file\", file);\n\n if (folderPath) {\n formData.append(\"folderPath\", folderPath);\n }\n\n const result = await uploadFile(url, formData, options?.progress);\n\n return {\n value: \"\",\n text: result.FileName\n };\n}\n\n/**\n * Uploads a BinaryFile into Rock. The specific storage location is defined by\n * the file type.\n *\n * @param file The file to be uploaded into Rock.\n * @param binaryFileTypeGuid The unique identifier of the BinaryFileType to handle the upload.\n * @param options The options ot use when uploading the file.\n *\n * @returns A ListItemBag whose value contains the new file Guid and text specifies the filename.\n */\nexport async function uploadBinaryFile(file: File, binaryFileTypeGuid: Guid, options?: UploadOptions): Promise {\n let url = `${options?.baseUrl ?? \"/FileUploader.ashx\"}?isBinaryFile=True&fileTypeGuid=${binaryFileTypeGuid}`;\n\n // Assume file is temporary unless specified otherwise so that files\n // that don't end up getting used will get cleaned up.\n if (options?.isTemporary === false) {\n url += \"&isTemporary=False\";\n }\n else {\n url += \"&isTemporary=True\";\n }\n\n const formData = new FormData();\n formData.append(\"file\", file);\n\n const result = await uploadFile(url, formData, options?.progress);\n\n return {\n value: result.Guid,\n text: result.FileName\n };\n}\n\n// #endregion\n\nexport default {\n doApiCall,\n post,\n get\n};\n","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n//\n\nimport { HttpResult } from \"@Obsidian/Types/Utility/http\";\nimport { AddressControlBag } from \"@Obsidian/ViewModels/Controls/addressControlBag\";\nimport { AddressControlValidateAddressOptionsBag } from \"@Obsidian/ViewModels/Rest/Controls/AddressControlValidateAddressOptionsBag\";\nimport { AddressControlValidateAddressResultsBag } from \"@Obsidian/ViewModels/Rest/Controls/AddressControlValidateAddressResultsBag\";\nimport { post } from \"./http\";\n\nexport function getDefaultAddressControlModel(): AddressControlBag {\n return {\n state: \"AZ\",\n country: \"US\"\n };\n}\n\nexport function validateAddress(address: AddressControlValidateAddressOptionsBag): Promise> {\n return post(\"/api/v2/Controls/AddressControlValidateAddress\", undefined, address);\n}\n\nexport function getAddressString(address: AddressControlBag): Promise> {\n return post(\"/api/v2/Controls/AddressControlGetStreetAddressString\", undefined, address);\n}","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n//\n\n/**\n * Flatten a nested array down by the given number of levels.\n * Meant to be a replacement for the official Array.prototype.flat, which isn't supported by all browsers we support.\n * Adapted from Polyfill: https://github.com/behnammodi/polyfill/blob/master/array.polyfill.js#L591\n *\n * @param arr (potentially) nested array to be flattened\n * @param depth The depth level specifying how deep a nested array structure should be flattened. Defaults to 1.\n *\n * @returns A new array with the sub-array elements concatenated into it.\n */\nexport const flatten = (arr: T[][], depth: number = 1): T[] => {\n const result: T[] = [];\n const forEach = result.forEach;\n\n const flatDeep = function (arr, depth): void {\n forEach.call(arr, function (val) {\n if (depth > 0 && Array.isArray(val)) {\n flatDeep(val, depth - 1);\n }\n else {\n result.push(val);\n }\n });\n };\n\n flatDeep(arr, depth);\n return result;\n};","/**\n * A function that will select a value from the object.\n */\ntype ValueSelector = (value: T) => string | number | boolean | null | undefined;\n\n/**\n * A function that will perform testing on a value to see if it meets\n * a certain condition and return true or false.\n */\ntype PredicateFn = (value: T, index: number) => boolean;\n\n/**\n * A function that will compare two values to see which one should\n * be ordered first.\n */\ntype ValueComparer = (a: T, b: T) => number;\n\nconst moreThanOneElement = \"More than one element was found in collection.\";\n\nconst noElementsFound = \"No element was found in collection.\";\n\n/**\n * Compares the values of two objects given the selector function.\n *\n * For the purposes of a compare, null and undefined are always a lower\n * value - unless both values are null or undefined in which case they\n * are considered equal.\n * \n * @param keySelector The function that will select the value.\n * @param descending True if this comparison should be in descending order.\n */\nfunction valueComparer(keySelector: ValueSelector, descending: boolean): ValueComparer {\n return (a: T, b: T): number => {\n const valueA = keySelector(a);\n const valueB = keySelector(b);\n\n // If valueA is null or undefined then it will either be considered\n // lower than or equal to valueB.\n if (valueA === undefined || valueA === null) {\n // If valueB is also null or undefined then they are considered equal.\n if (valueB === undefined || valueB === null) {\n return 0;\n }\n\n return !descending ? -1 : 1;\n }\n\n // If valueB is undefined or null (but valueA is not) then it is considered\n // a lower value than valueA.\n if (valueB === undefined || valueB === null) {\n return !descending ? 1 : -1;\n }\n\n // Perform a normal comparison.\n if (valueA > valueB) {\n return !descending ? 1 : -1;\n }\n else if (valueA < valueB) {\n return !descending ? -1 : 1;\n }\n else {\n return 0;\n }\n };\n}\n\n\n/**\n * Provides LINQ style access to an array of elements.\n */\nexport class List {\n /** The elements being tracked by this list. */\n protected elements: T[];\n\n // #region Constructors\n\n /**\n * Creates a new list with the given elements.\n * \n * @param elements The elements to be made available to LINQ queries.\n */\n constructor(elements?: T[]) {\n if (elements === undefined) {\n this.elements = [];\n }\n else {\n // Copy the array so if the caller makes changes it won't be reflected by us.\n this.elements = [...elements];\n }\n }\n\n /**\n * Creates a new List from the elements without copying to a new array.\n * \n * @param elements The elements to initialize the list with.\n * @returns A new list of elements.\n */\n public static fromArrayNoCopy(elements: T[]): List {\n const list = new List();\n\n list.elements = elements;\n\n return list;\n }\n\n // #endregion\n\n /**\n * Returns a boolean that determines if the collection contains any elements.\n *\n * @returns true if the collection contains any elements; otherwise false.\n */\n public any(): boolean;\n\n /**\n * Filters the list by the predicate and then returns a boolean that determines\n * if the filtered collection contains any elements.\n *\n * @param predicate The predicate to filter the elements by.\n *\n * @returns true if the collection contains any elements; otherwise false.\n */\n public any(predicate: PredicateFn): boolean;\n\n /**\n * Filters the list by the predicate and then returns a boolean that determines\n * if the filtered collection contains any elements.\n *\n * @param predicate The predicate to filter the elements by.\n *\n * @returns true if the collection contains any elements; otherwise false.\n */\n public any(predicate?: PredicateFn): boolean {\n let elements = this.elements;\n\n if (predicate !== undefined) {\n elements = elements.filter(predicate);\n }\n\n return elements.length > 0;\n }\n\n /**\n * Returns the first element from the collection if there are any elements.\n * Otherwise will throw an exception.\n *\n * @returns The first element in the collection.\n */\n public first(): T;\n\n /**\n * Filters the list by the predicate and then returns the first element\n * in the collection if any remain. Otherwise throws an exception.\n *\n * @param predicate The predicate to filter the elements by.\n *\n * @returns The first element in the collection.\n */\n public first(predicate: PredicateFn): T;\n\n /**\n * Filters the list by the predicate and then returns the first element\n * in the collection if any remain. Otherwise throws an exception.\n *\n * @param predicate The predicate to filter the elements by.\n *\n * @returns The first element in the collection.\n */\n public first(predicate?: PredicateFn): T {\n let elements = this.elements;\n\n if (predicate !== undefined) {\n elements = elements.filter(predicate);\n }\n\n if (elements.length >= 1) {\n return elements[0];\n }\n else {\n throw noElementsFound;\n }\n }\n\n /**\n * Returns the first element found in the collection or undefined if the\n * collection contains no elements.\n *\n * @returns The first element in the collection or undefined.\n */\n public firstOrUndefined(): T | undefined;\n\n /**\n * Filters the list by the predicate and then returns the first element\n * found in the collection. If no elements remain then undefined is\n * returned instead.\n *\n * @param predicate The predicate to filter the elements by.\n *\n * @returns The first element in the filtered collection or undefined.\n */\n public firstOrUndefined(predicate: PredicateFn): T | undefined;\n\n /**\n * Filters the list by the predicate and then returns the first element\n * found in the collection. If no elements remain then undefined is\n * returned instead.\n *\n * @param predicate The predicate to filter the elements by.\n *\n * @returns The first element in the filtered collection or undefined.\n */\n public firstOrUndefined(predicate?: PredicateFn): T | undefined {\n let elements = this.elements;\n\n if (predicate !== undefined) {\n elements = elements.filter(predicate);\n }\n\n if (elements.length === 1) {\n return elements[0];\n }\n else {\n return undefined;\n }\n }\n\n /**\n * Returns a single element from the collection if there is a single\n * element. Otherwise will throw an exception.\n *\n * @returns An element.\n */\n public single(): T;\n\n /**\n * Filters the list by the predicate and then returns the single remaining\n * element from the collection. If more than one element remains then an\n * exception will be thrown.\n *\n * @param predicate The predicate to filter the elements by.\n *\n * @returns An element.\n */\n public single(predicate: PredicateFn): T;\n\n /**\n * Filters the list by the predicate and then returns the single remaining\n * element from the collection. If more than one element remains then an\n * exception will be thrown.\n *\n * @param predicate The predicate to filter the elements by.\n *\n * @returns An element.\n */\n public single(predicate?: PredicateFn): T {\n let elements = this.elements;\n\n if (predicate !== undefined) {\n elements = elements.filter(predicate);\n }\n\n if (elements.length === 1) {\n return elements[0];\n }\n else {\n throw moreThanOneElement;\n }\n }\n\n /**\n * Returns a single element from the collection if there is a single\n * element. If no elements are found then undefined is returned. More\n * than a single element will throw an exception.\n *\n * @returns An element or undefined.\n */\n public singleOrUndefined(): T | undefined;\n\n /**\n * Filters the list by the predicate and then returns the single element\n * from the collection if there is only one remaining. If no elements\n * remain then undefined is returned. More than a single element will throw\n * an exception.\n *\n * @param predicate The predicate to filter the elements by.\n *\n * @returns An element or undefined.\n */\n public singleOrUndefined(predicate: PredicateFn): T | undefined;\n\n /**\n * Filters the list by the predicate and then returns the single element\n * from the collection if there is only one remaining. If no elements\n * remain then undefined is returned. More than a single element will throw\n * an exception.\n *\n * @param predicate The predicate to filter the elements by.\n *\n * @returns An element or undefined.\n */\n public singleOrUndefined(predicate?: PredicateFn): T | undefined {\n let elements = this.elements;\n\n if (predicate !== undefined) {\n elements = elements.filter(predicate);\n }\n\n if (elements.length === 0) {\n return undefined;\n }\n else if (elements.length === 1) {\n return elements[0];\n }\n else {\n throw moreThanOneElement;\n }\n }\n\n /**\n * Orders the elements of the array and returns a new list of items\n * in that order.\n * \n * @param keySelector The selector for the key to be ordered by.\n * @returns A new ordered list of elements.\n */\n public orderBy(keySelector: ValueSelector): OrderedList {\n const comparer = valueComparer(keySelector, false);\n\n return new OrderedList(this.elements, comparer);\n }\n\n /**\n * Orders the elements of the array in descending order and returns a\n * new list of items in that order.\n *\n * @param keySelector The selector for the key to be ordered by.\n * @returns A new ordered list of elements.\n */\n public orderByDescending(keySelector: ValueSelector): OrderedList {\n const comparer = valueComparer(keySelector, true);\n\n return new OrderedList(this.elements, comparer);\n }\n\n /**\n * Filters the results and returns a new list containing only the elements\n * that match the predicate.\n * \n * @param predicate The predicate to filter elements with.\n * \n * @returns A new collection of elements that match the predicate.\n */\n public where(predicate: PredicateFn): List {\n return new List(this.elements.filter(predicate));\n }\n\n /**\n * Get the elements of this list as a native array of items.\n *\n * @returns An array of items with all filters applied.\n */\n public toArray(): T[] {\n return [...this.elements];\n }\n}\n\n/**\n * A list of items that has ordering already applied.\n */\nclass OrderedList extends List {\n /** The base comparer to use when ordering. */\n private baseComparer!: ValueComparer;\n\n // #region Constructors\n\n constructor(elements: T[], baseComparer: ValueComparer) {\n super(elements);\n\n this.baseComparer = baseComparer;\n this.elements.sort(this.baseComparer);\n }\n\n // #endregion\n\n /**\n * Orders the elements of the array and returns a new list of items\n * in that order.\n * \n * @param keySelector The selector for the key to be ordered by.\n * @returns A new ordered list of elements.\n */\n public thenBy(keySelector: ValueSelector): OrderedList {\n const comparer = valueComparer(keySelector, false);\n\n return new OrderedList(this.elements, (a: T, b: T) => this.baseComparer(a, b) || comparer(a, b));\n }\n\n /**\n * Orders the elements of the array in descending order and returns a\n * new list of items in that order.\n *\n * @param keySelector The selector for the key to be ordered by.\n * @returns A new ordered list of elements.\n */\n public thenByDescending(keySelector: ValueSelector): OrderedList {\n const comparer = valueComparer(keySelector, true);\n\n return new OrderedList(this.elements, (a: T, b: T) => this.baseComparer(a, b) || comparer(a, b));\n }\n}\n","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n//\n\nimport { Guid } from \"@Obsidian/Types\";\n\n/** An empty unique identifier. */\nexport const emptyGuid = \"00000000-0000-0000-0000-000000000000\";\n\n/**\n* Generates a new Guid\n*/\nexport function newGuid (): Guid {\n return \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\".replace(/[xy]/g, (c) => {\n const r = Math.random() * 16 | 0;\n const v = c === \"x\" ? r : r & 0x3 | 0x8;\n return v.toString(16);\n });\n}\n\n/**\n * Returns a normalized Guid that can be compared with string equality (===)\n * @param a\n */\nexport function normalize (a: Guid | null | undefined): Guid | null {\n if (!a) {\n return null;\n }\n\n return a.toLowerCase();\n}\n\n/**\n * Checks if the given string is a valid Guid. To be considered valid it must\n * be a bare guid with hyphens. Bare means not enclosed in '{' and '}'.\n * \n * @param guid The Guid to be checked.\n * @returns True if the guid is valid, otherwise false.\n */\nexport function isValidGuid(guid: Guid | string): boolean {\n return /^[0-9A-Fa-f]{8}-(?:[0-9A-Fa-f]{4}-){3}[0-9A-Fa-f]{12}$/.test(guid);\n}\n\n/**\n * Converts the string value to a Guid.\n * \n * @param value The value to be converted.\n * @returns A Guid value or null is the string could not be parsed as a Guid.\n */\nexport function toGuidOrNull(value: string | null | undefined): Guid | null {\n if (value === null || value === undefined) {\n return null;\n }\n\n if (!isValidGuid(value)) {\n return null;\n }\n\n return value as Guid;\n}\n\n/**\n * Are the guids equal?\n * @param a\n * @param b\n */\nexport function areEqual (a: Guid | null | undefined, b: Guid | null | undefined): boolean {\n return normalize(a) === normalize(b);\n}\n\nexport default {\n newGuid,\n normalize,\n areEqual\n};\n\n","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n//\n\nimport { areEqual, toGuidOrNull } from \"./guid\";\nimport { Pluralize } from \"@Obsidian/Libs/pluralize\";\n\n/**\n * Is the value an empty string?\n * @param val\n */\nexport function isEmpty(val: unknown): boolean {\n if (typeof val === \"string\") {\n return val.length === 0;\n }\n\n return false;\n}\n\n/**\n * Is the value an empty string?\n * @param val\n */\nexport function isWhiteSpace(val: unknown): boolean {\n if (typeof val === \"string\") {\n return val.trim().length === 0;\n }\n\n return false;\n}\n\n/**\n * Is the value null, undefined or whitespace?\n * @param val\n */\nexport function isNullOrWhiteSpace(val: unknown): boolean {\n return isWhiteSpace(val) || val === undefined || val === null;\n}\n\n/**\n * Turns camelCase or PascalCase strings into separate strings - \"MyCamelCaseString\" turns into \"My Camel Case String\"\n * @param val\n */\nexport function splitCase(val: string): string {\n // First, insert a space before sequences of capital letters followed by a lowercase letter (e.g., \"RESTKey\" -> \"REST Key\")\n val = val.replace(/([A-Z]+)([A-Z][a-z])/g, \"$1 $2\");\n // Then, insert a space before sequences of a lowercase letter or number followed by a capital letter (e.g., \"myKey\" -> \"my Key\")\n return val.replace(/([a-z0-9])([A-Z])/g, \"$1 $2\");\n}\n\n/**\n * Returns a string that has each item comma separated except for the last\n * which will use the word \"and\".\n *\n * @example\n * ['a', 'b', 'c'] => 'a, b and c'\n *\n * @param strs The strings to be joined.\n * @param andStr The custom string to use instead of the word \"and\".\n *\n * @returns A string that represents all the strings.\n */\nexport function asCommaAnd(strs: string[], andStr?: string): string {\n if (strs.length === 0) {\n return \"\";\n }\n\n if (strs.length === 1) {\n return strs[0];\n }\n\n if (!andStr) {\n andStr = \"and\";\n }\n\n if (strs.length === 2) {\n return `${strs[0]} ${andStr} ${strs[1]}`;\n }\n\n const last = strs.pop();\n return `${strs.join(\", \")} ${andStr} ${last}`;\n}\n\n/**\n * Convert the string to the title case.\n * hellO worlD => Hello World\n * @param str\n */\nexport function toTitleCase(str: string | null): string {\n if (!str) {\n return \"\";\n }\n\n return str.replace(/\\w\\S*/g, (word) => {\n return word.charAt(0).toUpperCase() + word.substring(1).toLowerCase();\n });\n}\n\n/**\n * Capitalize the first character\n */\nexport function upperCaseFirstCharacter(str: string | null): string {\n if (!str) {\n return \"\";\n }\n\n return str.charAt(0).toUpperCase() + str.substring(1);\n}\n\n/**\n * Pluralizes the given word. If count is specified and is equal to 1 then\n * the singular form of the word is returned. This will also de-pluralize a\n * word if required.\n *\n * @param word The word to be pluralized or singularized.\n * @param count An optional count to indicate when the word should be singularized.\n *\n * @returns The word in plural or singular form depending on the options.\n */\nexport function pluralize(word: string, count?: number): string {\n return Pluralize(word, count);\n}\n\n/**\n * Returns a singular or plural phrase depending on if the number is 1.\n * (0, Cat, Cats) => Cats\n * (1, Cat, Cats) => Cat\n * (2, Cat, Cats) => Cats\n * @param num\n * @param singular\n * @param plural\n */\nexport function pluralConditional(num: number, singular: string, plural: string): string {\n return num === 1 ? singular : plural;\n}\n\n/**\n * Pad the left side of a string so it is at least length characters long.\n *\n * @param str The string to be padded.\n * @param length The minimum length to make the string.\n * @param padCharacter The character to use to pad the string.\n */\nexport function padLeft(str: string | undefined | null, length: number, padCharacter: string = \" \"): string {\n if (padCharacter == \"\") {\n padCharacter = \" \";\n }\n else if (padCharacter.length > 1) {\n padCharacter = padCharacter.substring(0, 1);\n }\n\n if (!str) {\n return Array(length + 1).join(padCharacter);\n }\n\n if (str.length >= length) {\n return str;\n }\n\n return Array(length - str.length + 1).join(padCharacter) + str;\n}\n\n/**\n * Pad the right side of a string so it is at least length characters long.\n *\n * @param str The string to be padded.\n * @param length The minimum length to make the string.\n * @param padCharacter The character to use to pad the string.\n */\nexport function padRight(str: string | undefined | null, length: number, padCharacter: string = \" \"): string {\n if (padCharacter == \"\") {\n padCharacter = \" \";\n }\n else if (padCharacter.length > 1) {\n padCharacter = padCharacter.substring(0, 1);\n }\n\n if (!str) {\n return Array(length).join(padCharacter);\n }\n\n if (str.length >= length) {\n return str;\n }\n\n return str + Array(length - str.length + 1).join(padCharacter);\n}\n\nexport type TruncateOptions = {\n ellipsis?: boolean;\n};\n\n/**\n * Ensure a string does not go over the character limit. Truncation happens\n * on word boundaries.\n *\n * @param str The string to be truncated.\n * @param limit The maximum length of the resulting string.\n * @param options Additional options that control how truncation will happen.\n *\n * @returns The truncated string.\n */\nexport function truncate(str: string, limit: number, options?: TruncateOptions): string {\n // Early out if the string is already under the limit.\n if (str.length <= limit) {\n return str;\n }\n\n // All the whitespace characters that we can split on.\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\";\n const reg = new RegExp(`(?=[${trimmable}])`);\n const words = str.split(reg);\n let count = 0;\n\n // If we are appending ellipsis, then shorten the limit size.\n if (options && options.ellipsis === true) {\n limit -= 3;\n }\n\n // Get a list of words that will fit within our length requirements.\n const visibleWords = words.filter(function (word) {\n count += word.length;\n return count <= limit;\n });\n\n return `${visibleWords.join(\"\")}...`;\n}\n\n/** The regular expression that contains the characters to be escaped. */\nconst escapeHtmlRegExp = /[\"'&<>]/g;\n\n/** The character map of the characters to be replaced and the strings to replace them with. */\nconst escapeHtmlMap: Record = {\n '\"': \""\",\n \"&\": \"&\",\n \"'\": \"'\",\n \"<\": \"<\",\n \">\": \">\"\n};\n\n/**\n * Escapes a string so it can be used in HTML. This turns things like the <\n * character into the < sequence so it will still render as \"<\".\n *\n * @param str The string to be escaped.\n * @returns A string that has all HTML entities escaped.\n */\nexport function escapeHtml(str: string): string {\n return str.replace(escapeHtmlRegExp, (ch) => {\n return escapeHtmlMap[ch];\n });\n}\n\n/**\n * The default compare value function for UI controls. This checks if both values\n * are GUIDs and if so does a case-insensitive compare, otherwise it does a\n * case-sensitive compare of the two values.\n *\n * @param value The value selected in the UI.\n * @param itemValue The item value to be compared against.\n *\n * @returns true if the two values are considered equal; otherwise false.\n */\nexport function defaultControlCompareValue(value: string, itemValue: string): boolean {\n const guidValue = toGuidOrNull(value);\n const guidItemValue = toGuidOrNull(itemValue);\n\n if (guidValue !== null && guidItemValue !== null) {\n return areEqual(guidValue, guidItemValue);\n }\n\n return value === itemValue;\n}\n\n/**\n * Determins whether or not a given string contains any HTML tags in.\n *\n * @param value The string potentially containing HTML\n *\n * @returns true if it contains HTML, otherwise false\n */\nexport function containsHtmlTag(value: string): boolean {\n return /<[/0-9a-zA-Z]/.test(value);\n}\n\nexport default {\n asCommaAnd,\n containsHtmlTag,\n escapeHtml,\n splitCase,\n isNullOrWhiteSpace,\n isWhiteSpace,\n isEmpty,\n toTitleCase,\n pluralConditional,\n padLeft,\n padRight,\n truncate\n};\n","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n//\n\n/**\n * A helper object that provides equivalent format strings for a given locale,\n * for the various date libraries used throughout Rock.\n *\n * This API is internal to Rock, and is not subject to the same compatibility\n * standards as public APIs. It may be changed or removed without notice in any\n * release. You should not use this API directly in any plug-ins. Doing so can\n * result in application failures when updating to a new Rock release.\n */\nexport class LocaleDateFormatter {\n /**\n * The internal JavaScript date format string for the locale represented\n * by this formatter instance.\n */\n private jsDateFormatString: string;\n\n /**\n * The internal ASP C# date format string for the locale represented by this\n * formatter instance.\n */\n private aspDateFormatString: string | undefined;\n\n /**\n * The internal date picker format string for the locale represented by this\n * formatter instance.\n */\n private datePickerFormatString: string | undefined;\n\n /**\n * Creates a new instance of LocaleDateFormatter.\n *\n * @param jsDateFormatString The JavaScript date format string for the\n * locale represented by this formatter instance.\n * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date#date_time_string_format\n */\n private constructor(jsDateFormatString: string) {\n this.jsDateFormatString = jsDateFormatString;\n }\n\n /**\n * Creates a new instance of LocaleDateFormatter from the current locale. If\n * the current locale cannot be determined, a default \"en-US\" locale\n * formatter instance will be returned.\n *\n * @returns A LocaleDateFormatter instance representing the current locale.\n */\n public static fromCurrent(): LocaleDateFormatter {\n // Create an arbitrary date with recognizable numeric parts; format the\n // date using the current locale settings and then replace the numeric\n // parts with date format placeholders to get the locale date format\n // string. Note that month is specified as an index in the Date\n // constructor, so \"2\" represents month \"3\".\n const date = new Date(2222, 2, 4);\n const localeDateString = date.toLocaleDateString(undefined, {\n year: \"numeric\",\n month: \"numeric\",\n day: \"numeric\"\n });\n\n // Fall back to a default, en-US format string if any step of the\n // parsing fails.\n const defaultFormatString = \"MM/DD/YYYY\";\n\n let localeFormatString = localeDateString;\n\n // Replace the known year date part with a 2 or 4 digit format string.\n if (localeDateString.includes(\"2222\")) {\n localeFormatString = localeDateString\n .replace(\"2222\", \"YYYY\");\n }\n else if (localeDateString.includes(\"22\")) {\n localeFormatString = localeDateString\n .replace(\"22\", \"YY\");\n }\n else {\n return new LocaleDateFormatter(defaultFormatString);\n }\n\n // Replace the known month date part with a 1 or 2 digit format string.\n if (localeFormatString.includes(\"03\")) {\n localeFormatString = localeFormatString.replace(\"03\", \"MM\");\n }\n else if (localeFormatString.includes(\"3\")) {\n localeFormatString = localeFormatString.replace(\"3\", \"M\");\n }\n else {\n return new LocaleDateFormatter(defaultFormatString);\n }\n\n // Replace the known day date part with a 1 or 2 digit format string.\n if (localeFormatString.includes(\"04\")) {\n localeFormatString = localeFormatString.replace(\"04\", \"DD\");\n }\n else if (localeFormatString.includes(\"4\")) {\n localeFormatString = localeFormatString.replace(\"4\", \"D\");\n }\n else {\n return new LocaleDateFormatter(defaultFormatString);\n }\n\n return new LocaleDateFormatter(localeFormatString);\n }\n\n /**\n * The ASP C# date format string for the locale represented by this\n * formatter instance.\n */\n public get aspDateFormat(): string {\n if (!this.aspDateFormatString) {\n // Transform the standard JavaScript format string to follow C# date\n // formatting rules.\n // https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings\n this.aspDateFormatString = this.jsDateFormatString\n .replace(/D/g, \"d\")\n .replace(/Y/g, \"y\");\n }\n\n return this.aspDateFormatString;\n }\n\n /**\n * The date picker format string for the locale represented by this\n * formatter instance.\n */\n public get datePickerFormat(): string {\n if (!this.datePickerFormatString) {\n // Transform the standard JavaScript format string to follow the\n // bootstrap-datepicker library's formatting rules.\n // https://bootstrap-datepicker.readthedocs.io/en/stable/options.html#format\n this.datePickerFormatString = this.jsDateFormatString\n .replace(/D/g, \"d\")\n .replace(/M/g, \"m\")\n .replace(/Y/g, \"y\");\n }\n\n return this.datePickerFormatString;\n }\n}\n","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n//\n\nimport { List } from \"./linq\";\nimport { padLeft, padRight } from \"./stringUtils\";\nimport { RockDateTime } from \"./rockDateTime\";\nimport { LocaleDateFormatter } from \"./localeDateFormatter\";\n\n/**\n * Returns a blank string if the string value is 0.\n *\n * @param value The value to check and return.\n * @returns The value passed in or an empty string if it equates to zero.\n */\nfunction blankIfZero(value: string): string {\n return parseInt(value) === 0 ? \"\" : value;\n}\n\n/**\n * Gets the 12 hour value of the given 24-hour number.\n *\n * @param hour The hour in a 24-hour format.\n * @returns The hour in a 12-hour format.\n */\nfunction get12HourValue(hour: number): number {\n if (hour == 0) {\n return 12;\n }\n else if (hour < 13) {\n return hour;\n }\n else {\n return hour - 12;\n }\n}\ntype DateFormatterCommand = (date: RockDateTime) => string;\n\nconst englishDayNames = [\"Sunday\", \"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\", \"Saturday\"];\nconst englishMonthNames = [\"January\", \"February\", \"March\", \"April\", \"May\", \"June\", \"July\", \"August\", \"September\", \"October\", \"November\", \"December\"];\n\nconst dateFormatters: Record = {\n \"yyyyy\": date => padLeft(date.year.toString(), 5, \"0\"),\n \"yyyy\": date => padLeft(date.year.toString(), 4, \"0\"),\n \"yyy\": date => padLeft(date.year.toString(), 3, \"0\"),\n \"yy\": date => padLeft((date.year % 100).toString(), 2, \"0\"),\n \"y\": date => (date.year % 100).toString(),\n\n \"MMMM\": date => englishMonthNames[date.month - 1],\n \"MMM\": date => englishMonthNames[date.month - 1].substr(0, 3),\n \"MM\": date => padLeft(date.month.toString(), 2, \"0\"),\n \"M\": date => date.month.toString(),\n\n \"dddd\": date => englishDayNames[date.dayOfWeek],\n \"ddd\": date => englishDayNames[date.dayOfWeek].substr(0, 3),\n \"dd\": date => padLeft(date.day.toString(), 2, \"0\"),\n \"d\": date => date.day.toString(),\n\n \"fffffff\": date => padRight((date.millisecond * 10000).toString(), 7, \"0\"),\n \"ffffff\": date => padRight((date.millisecond * 1000).toString(), 6, \"0\"),\n \"fffff\": date => padRight((date.millisecond * 100).toString(), 5, \"0\"),\n \"ffff\": date => padRight((date.millisecond * 10).toString(), 4, \"0\"),\n \"fff\": date => padRight(date.millisecond.toString(), 3, \"0\"),\n \"ff\": date => padRight(Math.floor(date.millisecond / 10).toString(), 2, \"0\"),\n \"f\": date => padRight(Math.floor(date.millisecond / 100).toString(), 1, \"0\"),\n\n \"FFFFFFF\": date => blankIfZero(padRight((date.millisecond * 10000).toString(), 7, \"0\")),\n \"FFFFFF\": date => blankIfZero(padRight((date.millisecond * 1000).toString(), 6, \"0\")),\n \"FFFFF\": date => blankIfZero(padRight((date.millisecond * 100).toString(), 5, \"0\")),\n \"FFFF\": date => blankIfZero(padRight((date.millisecond * 10).toString(), 4, \"0\")),\n \"FFF\": date => blankIfZero(padRight(date.millisecond.toString(), 3, \"0\")),\n \"FF\": date => blankIfZero(padRight(Math.floor(date.millisecond / 10).toString(), 2, \"0\")),\n \"F\": date => blankIfZero(padRight(Math.floor(date.millisecond / 100).toString(), 1, \"0\")),\n\n \"g\": date => date.year < 0 ? \"B.C.\" : \"A.D.\",\n \"gg\": date => date.year < 0 ? \"B.C.\" : \"A.D.\",\n\n \"hh\": date => padLeft(get12HourValue(date.hour).toString(), 2, \"0\"),\n \"h\": date => get12HourValue(date.hour).toString(),\n\n \"HH\": date => padLeft(date.hour.toString(), 2, \"0\"),\n \"H\": date => date.hour.toString(),\n\n \"mm\": date => padLeft(date.minute.toString(), 2, \"0\"),\n \"m\": date => date.minute.toString(),\n\n \"ss\": date => padLeft(date.second.toString(), 2, \"0\"),\n \"s\": date => date.second.toString(),\n\n \"K\": date => {\n const offset = date.offset;\n const offsetHour = Math.abs(Math.floor(offset / 60));\n const offsetMinute = Math.abs(offset % 60);\n return `${offset >= 0 ? \"+\" : \"-\"}${padLeft(offsetHour.toString(), 2, \"0\")}:${padLeft(offsetMinute.toString(), 2, \"0\")}`;\n },\n\n \"tt\": date => date.hour >= 12 ? \"PM\" : \"AM\",\n \"t\": date => date.hour >= 12 ? \"P\" : \"A\",\n\n \"zzz\": date => {\n const offset = date.offset;\n const offsetHour = Math.abs(Math.floor(offset / 60));\n const offsetMinute = Math.abs(offset % 60);\n return `${offset >= 0 ? \"+\" : \"-\"}${padLeft(offsetHour.toString(), 2, \"0\")}:${padLeft(offsetMinute.toString(), 2, \"0\")}`;\n },\n \"zz\": date => {\n const offset = date.offset;\n const offsetHour = Math.abs(Math.floor(offset / 60));\n return `${offset >= 0 ? \"+\" : \"-\"}${padLeft(offsetHour.toString(), 2, \"0\")}`;\n },\n \"z\": date => {\n const offset = date.offset;\n const offsetHour = Math.abs(Math.floor(offset / 60));\n return `${offset >= 0 ? \"+\" : \"-\"}${offsetHour}`;\n },\n\n \":\": () => \":\",\n \"/\": () => \"/\"\n};\n\nconst dateFormatterKeys = new List(Object.keys(dateFormatters))\n .orderByDescending(k => k.length)\n .toArray();\n\nconst currentLocaleDateFormatter = LocaleDateFormatter.fromCurrent();\n\nconst standardDateFormats: Record = {\n \"d\": date => formatAspDate(date, currentLocaleDateFormatter.aspDateFormat),\n \"D\": date => formatAspDate(date, \"dddd, MMMM dd, yyyy\"),\n \"t\": date => formatAspDate(date, \"h:mm tt\"),\n \"T\": date => formatAspDate(date, \"h:mm:ss tt\"),\n \"M\": date => formatAspDate(date, \"MMMM dd\"),\n \"m\": date => formatAspDate(date, \"MMMM dd\"),\n \"Y\": date => formatAspDate(date, \"yyyy MMMM\"),\n \"y\": date => formatAspDate(date, \"yyyy MMMM\"),\n \"f\": date => `${formatAspDate(date, \"D\")} ${formatAspDate(date, \"t\")}`,\n \"F\": date => `${formatAspDate(date, \"D\")} ${formatAspDate(date, \"T\")}`,\n \"g\": date => `${formatAspDate(date, \"d\")} ${formatAspDate(date, \"t\")}`,\n \"G\": date => `${formatAspDate(date, \"d\")} ${formatAspDate(date, \"T\")}`,\n \"o\": date => formatAspDate(date, `yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffffzzz`),\n \"O\": date => formatAspDate(date, `yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffffzzz`),\n \"r\": date => formatAspDate(date, `ddd, dd MMM yyyy HH':'mm':'ss 'GMT'`),\n \"R\": date => formatAspDate(date, `ddd, dd MMM yyyy HH':'mm':'ss 'GMT'`),\n \"s\": date => formatAspDate(date, `yyyy'-'MM'-'dd'T'HH':'mm':'ss`),\n \"u\": date => formatAspDate(date, `yyyy'-'MM'-'dd HH':'mm':'ss'Z'`),\n \"U\": date => {\n return formatAspDate(date.universalDateTime, `F`);\n },\n};\n\n/**\n * Formats the Date object using custom format specifiers.\n *\n * @param date The date object to be formatted.\n * @param format The custom format string.\n * @returns A string that represents the date in the specified format.\n */\nfunction formatAspCustomDate(date: RockDateTime, format: string): string {\n let result = \"\";\n\n for (let i = 0; i < format.length;) {\n let matchFound = false;\n\n for (const k of dateFormatterKeys) {\n if (format.substr(i, k.length) === k) {\n result += dateFormatters[k](date);\n matchFound = true;\n i += k.length;\n break;\n }\n }\n\n if (matchFound) {\n continue;\n }\n\n if (format[i] === \"\\\\\") {\n i++;\n if (i < format.length) {\n result += format[i++];\n }\n }\n else if (format[i] === \"'\") {\n i++;\n for (; i < format.length && format[i] !== \"'\"; i++) {\n result += format[i];\n }\n i++;\n }\n else if (format[i] === '\"') {\n i++;\n for (; i < format.length && format[i] !== '\"'; i++) {\n result += format[i];\n }\n i++;\n }\n else {\n result += format[i++];\n }\n }\n\n return result;\n}\n\n/**\n * Formats the Date object using a standard format string.\n *\n * @param date The date object to be formatted.\n * @param format The standard format specifier.\n * @returns A string that represents the date in the specified format.\n */\nfunction formatAspStandardDate(date: RockDateTime, format: string): string {\n if (standardDateFormats[format] !== undefined) {\n return standardDateFormats[format](date);\n }\n\n return format;\n}\n\n/**\n * Formats the given Date object using nearly the same rules as the ASP C#\n * format methods.\n *\n * @param date The date object to be formatted.\n * @param format The format string to use.\n */\nexport function formatAspDate(date: RockDateTime, format: string): string {\n if (format.length === 1) {\n return formatAspStandardDate(date, format);\n }\n else if (format.length === 2 && format[0] === \"%\") {\n return formatAspCustomDate(date, format[1]);\n }\n else {\n return formatAspCustomDate(date, format);\n }\n}\n","import { DateTime, FixedOffsetZone, Zone } from \"luxon\";\nimport { formatAspDate } from \"./aspDateFormat\";\nimport { DayOfWeek } from \"@Obsidian/Enums/Controls/dayOfWeek\";\n\n/**\n * The days of the week that are used by RockDateTime.\n */\nexport { DayOfWeek } from \"@Obsidian/Enums/Controls/dayOfWeek\";\n\n/**\n * The various date and time formats supported by the formatting methods.\n */\nexport const DateTimeFormat: Record = {\n DateFull: {\n year: \"numeric\",\n month: \"long\",\n day: \"numeric\"\n },\n\n DateMedium: {\n year: \"numeric\",\n month: \"short\",\n day: \"numeric\"\n },\n\n DateShort: {\n year: \"numeric\",\n month: \"numeric\",\n day: \"numeric\"\n },\n\n TimeShort: {\n hour: \"numeric\",\n minute: \"numeric\",\n },\n\n TimeWithSeconds: {\n hour: \"numeric\",\n minute: \"numeric\",\n second: \"numeric\"\n },\n\n DateTimeShort: {\n year: \"numeric\",\n month: \"numeric\",\n day: \"numeric\",\n hour: \"numeric\",\n minute: \"numeric\"\n },\n\n DateTimeShortWithSeconds: {\n year: \"numeric\",\n month: \"numeric\",\n day: \"numeric\",\n hour: \"numeric\",\n minute: \"numeric\",\n second: \"numeric\"\n },\n\n DateTimeMedium: {\n year: \"numeric\",\n month: \"short\",\n day: \"numeric\",\n hour: \"numeric\",\n minute: \"numeric\"\n },\n\n DateTimeMediumWithSeconds: {\n year: \"numeric\",\n month: \"short\",\n day: \"numeric\",\n hour: \"numeric\",\n minute: \"numeric\",\n second: \"numeric\"\n },\n\n DateTimeFull: {\n year: \"numeric\",\n month: \"long\",\n day: \"numeric\",\n hour: \"numeric\",\n minute: \"numeric\"\n },\n\n DateTimeFullWithSeconds: {\n year: \"numeric\",\n month: \"long\",\n day: \"numeric\",\n hour: \"numeric\",\n minute: \"numeric\",\n second: \"numeric\"\n }\n};\n\n/**\n * A date and time object that handles time zones and formatting. This class is\n * immutable and cannot be modified. All modifications are performed by returning\n * a new RockDateTime instance.\n */\nexport class RockDateTime {\n /** The internal DateTime object that holds our date information. */\n private dateTime: DateTime;\n\n // #region Constructors\n\n /**\n * Creates a new instance of RockDateTime.\n *\n * @param dateTime The Luxon DateTime object that is used to track the internal state.\n */\n private constructor(dateTime: DateTime) {\n this.dateTime = dateTime;\n }\n\n /**\n * Creates a new instance of RockDateTime from the given date and time parts.\n *\n * @param year The year of the new date.\n * @param month The month of the new date (1-12).\n * @param day The day of month of the new date.\n * @param hour The hour of the day.\n * @param minute The minute of the hour.\n * @param second The second of the minute.\n * @param millisecond The millisecond of the second.\n * @param zone The time zone offset to construct the date in.\n *\n * @returns A RockDateTime instance or null if the requested date was not valid.\n */\n public static fromParts(year: number, month: number, day: number, hour?: number, minute?: number, second?: number, millisecond?: number, zone?: number | string): RockDateTime | null {\n let luxonZone: Zone | string | undefined;\n\n if (zone !== undefined) {\n if (typeof zone === \"number\") {\n luxonZone = FixedOffsetZone.instance(zone);\n }\n else {\n luxonZone = zone;\n }\n }\n\n const dateTime = DateTime.fromObject({\n year,\n month,\n day,\n hour,\n minute,\n second,\n millisecond\n }, {\n zone: luxonZone\n });\n\n if (!dateTime.isValid) {\n return null;\n }\n\n return new RockDateTime(dateTime);\n }\n\n /**\n * Creates a new instance of RockDateTime that represents the time specified\n * as the Javascript milliseconds value. The time zone is set to the browser\n * time zone.\n *\n * @param milliseconds The time in milliseconds since the epoch.\n *\n * @returns A new RockDateTime instance or null if the specified date was not valid.\n */\n public static fromMilliseconds(milliseconds: number): RockDateTime | null {\n const dateTime = DateTime.fromMillis(milliseconds);\n\n if (!dateTime.isValid) {\n return null;\n }\n\n return new RockDateTime(dateTime);\n }\n\n /**\n * Construct a new RockDateTime instance from a Javascript Date object.\n *\n * @param date The Javascript date object that contains the date information.\n *\n * @returns A RockDateTime instance or null if the date was not valid.\n */\n public static fromJSDate(date: Date): RockDateTime | null {\n const dateTime = DateTime.fromJSDate(date);\n\n if (!dateTime.isValid) {\n return null;\n }\n\n return new RockDateTime(dateTime);\n }\n\n /**\n * Constructs a new RockDateTime instance by parsing the given string from\n * ISO 8601 format.\n *\n * @param dateString The string that contains the ISO 8601 formatted text.\n *\n * @returns A new RockDateTime instance or null if the date was not valid.\n */\n public static parseISO(dateString: string): RockDateTime | null {\n const dateTime = DateTime.fromISO(dateString, { setZone: true });\n\n if (!dateTime.isValid) {\n return null;\n }\n\n return new RockDateTime(dateTime);\n }\n\n /**\n * Constructs a new RockDateTime instance by parsing the given string from\n * RFC 1123 format. This is common in HTTP headers.\n *\n * @param dateString The string that contains the RFC 1123 formatted text.\n *\n * @returns A new RockDateTime instance or null if the date was not valid.\n */\n public static parseHTTP(dateString: string): RockDateTime | null {\n const dateTime = DateTime.fromHTTP(dateString, { setZone: true });\n\n if (!dateTime.isValid) {\n return null;\n }\n\n return new RockDateTime(dateTime);\n }\n\n /**\n * Creates a new RockDateTime instance that represents the current date and time.\n *\n * @returns A RockDateTime instance.\n */\n public static now(): RockDateTime {\n return new RockDateTime(DateTime.now());\n }\n\n /**\n * Creates a new RockDateTime instance that represents the current time in UTC.\n *\n * @returns A new RockDateTime instance in the UTC time zone.\n */\n public static utcNow(): RockDateTime {\n return new RockDateTime(DateTime.now().toUTC());\n }\n\n // #endregion\n\n // #region Properties\n\n /**\n * The Date portion of this RockDateTime instance. All time properties of\n * the returned instance will be set to 0.\n */\n public get date(): RockDateTime {\n const date = RockDateTime.fromParts(this.year, this.month, this.day, 0, 0, 0, 0, this.offset);\n\n if (date === null) {\n throw \"Could not convert to date instance.\";\n }\n\n return date;\n }\n\n /**\n * The raw date with no offset applied to it. Use this method when you only\n * care about comparing explicit dates without the time zone, as we do within\n * the grid's date column filter.\n *\n * This API is internal to Rock, and is not subject to the same compatibility\n * standards as public APIs. It may be changed or removed without notice in any\n * release. You should not use this API directly in any plug-ins. Doing so can\n * result in application failures when updating to a new Rock release.\n */\n public get rawDate(): RockDateTime {\n const date = RockDateTime.fromParts(this.year, this.month, this.day, 0, 0, 0, 0);\n\n if (date === null) {\n throw \"Could not convert to date instance.\";\n }\n\n return date;\n }\n\n /**\n * The day of the month represented by this instance.\n */\n public get day(): number {\n return this.dateTime.day;\n }\n\n /**\n * The day of the week represented by this instance.\n */\n public get dayOfWeek(): DayOfWeek {\n switch (this.dateTime.weekday) {\n case 1:\n return DayOfWeek.Monday;\n\n case 2:\n return DayOfWeek.Tuesday;\n\n case 3:\n return DayOfWeek.Wednesday;\n\n case 4:\n return DayOfWeek.Thursday;\n\n case 5:\n return DayOfWeek.Friday;\n\n case 6:\n return DayOfWeek.Saturday;\n\n case 7:\n return DayOfWeek.Sunday;\n }\n\n throw \"Could not determine day of week.\";\n }\n\n /**\n * The day of the year represented by this instance.\n */\n public get dayOfYear(): number {\n return this.dateTime.ordinal;\n }\n\n /**\n * The hour of the day represented by this instance.\n */\n public get hour(): number {\n return this.dateTime.hour;\n }\n\n /**\n * The millisecond of the second represented by this instance.\n */\n public get millisecond(): number {\n return this.dateTime.millisecond;\n }\n\n /**\n * The minute of the hour represented by this instance.\n */\n public get minute(): number {\n return this.dateTime.minute;\n }\n\n /**\n * The month of the year represented by this instance (1-12).\n */\n public get month(): number {\n return this.dateTime.month;\n }\n\n /**\n * The offset from UTC represented by this instance. If the timezone of this\n * instance is UTC-7 then the value returned is -420.\n */\n public get offset(): number {\n return this.dateTime.offset;\n }\n\n /**\n * The second of the minute represented by this instance.\n */\n public get second(): number {\n return this.dateTime.second;\n }\n\n /**\n * The year represented by this instance.\n */\n public get year(): number {\n return this.dateTime.year;\n }\n\n /**\n * Creates a new RockDateTime instance that represents the same point in\n * time represented in the local browser time zone.\n */\n public get localDateTime(): RockDateTime {\n return new RockDateTime(this.dateTime.toLocal());\n }\n\n /**\n * Creates a new RockDateTime instance that represents the same point in\n * time represented in the organization time zone.\n */\n public get organizationDateTime(): RockDateTime {\n throw \"Not Implemented\";\n }\n\n /**\n * Creates a new RockDateTime instance that represents the same point in\n * time represented in UTC.\n */\n public get universalDateTime(): RockDateTime {\n return new RockDateTime(this.dateTime.toUTC());\n }\n\n // #endregion\n\n // #region Methods\n\n /**\n * Creates a new RockDateTime instance that represents the date and time\n * after adding the number of days to this instance.\n *\n * @param days The number of days to add.\n *\n * @returns A new instance of RockDateTime that represents the new date and time.\n */\n public addDays(days: number): RockDateTime {\n const dateTime = this.dateTime.plus({ days: days });\n\n if (!dateTime.isValid) {\n throw \"Operation produced an invalid date.\";\n }\n\n return new RockDateTime(dateTime);\n }\n\n /**\n * Creates a new RockDateTime instance that represents the last millisecond\n * of the end of the month for this instance.\n *\n * @example\n * RockDateTime.fromJSDate(new Date(2014, 3, 3)).endOfMonth().toISOString(); //=> '2014-03-31T23:59:59.999-05:00'\n */\n public endOfMonth(): RockDateTime {\n const dateTime = this.dateTime.endOf(\"month\");\n\n if (!dateTime.isValid) {\n throw \"Operation produced an invalid date.\";\n }\n\n return new RockDateTime(dateTime);\n }\n\n /**\n * Creates a new RockDateTime instance that represents the date and time\n * after adding the number of hours to this instance.\n *\n * @param days The number of hours to add.\n *\n * @returns A new instance of RockDateTime that represents the new date and time.\n */\n public addHours(hours: number): RockDateTime {\n const dateTime = this.dateTime.plus({ hours: hours });\n\n if (!dateTime.isValid) {\n throw \"Operation produced an invalid date.\";\n }\n\n return new RockDateTime(dateTime);\n }\n\n /**\n * Creates a new RockDateTime instance that represents the date and time\n * after adding the number of milliseconds to this instance.\n *\n * @param days The number of milliseconds to add.\n *\n * @returns A new instance of RockDateTime that represents the new date and time.\n */\n public addMilliseconds(milliseconds: number): RockDateTime {\n const dateTime = this.dateTime.plus({ milliseconds: milliseconds });\n\n if (!dateTime.isValid) {\n throw \"Operation produced an invalid date.\";\n }\n\n return new RockDateTime(dateTime);\n }\n\n /**\n * Creates a new RockDateTime instance that represents the date and time\n * after adding the number of minutes to this instance.\n *\n * @param days The number of minutes to add.\n *\n * @returns A new instance of RockDateTime that represents the new date and time.\n */\n public addMinutes(minutes: number): RockDateTime {\n const dateTime = this.dateTime.plus({ minutes: minutes });\n\n if (!dateTime.isValid) {\n throw \"Operation produced an invalid date.\";\n }\n\n return new RockDateTime(dateTime);\n }\n\n /**\n * Creates a new RockDateTime instance that represents the date and time\n * after adding the number of months to this instance.\n *\n * @param days The number of months to add.\n *\n * @returns A new instance of RockDateTime that represents the new date and time.\n */\n public addMonths(months: number): RockDateTime {\n const dateTime = this.dateTime.plus({ months: months });\n\n if (!dateTime.isValid) {\n throw \"Operation produced an invalid date.\";\n }\n\n return new RockDateTime(dateTime);\n }\n\n /**\n * Creates a new RockDateTime instance that represents the date and time\n * after adding the number of seconds to this instance.\n *\n * @param days The number of seconds to add.\n *\n * @returns A new instance of RockDateTime that represents the new date and time.\n */\n public addSeconds(seconds: number): RockDateTime {\n const dateTime = this.dateTime.plus({ seconds: seconds });\n\n if (!dateTime.isValid) {\n throw \"Operation produced an invalid date.\";\n }\n\n return new RockDateTime(dateTime);\n }\n\n /**\n * Creates a new RockDateTime instance that represents the date and time\n * after adding the number of years to this instance.\n *\n * @param days The number of years to add.\n *\n * @returns A new instance of RockDateTime that represents the new date and time.\n */\n public addYears(years: number): RockDateTime {\n const dateTime = this.dateTime.plus({ years: years });\n\n if (!dateTime.isValid) {\n throw \"Operation produced an invalid date.\";\n }\n\n return new RockDateTime(dateTime);\n }\n\n /**\n * Converts the date time representation into the number of milliseconds\n * that have elapsed since the epoch (1970-01-01T00:00:00Z).\n *\n * @returns The number of milliseconds since the epoch.\n */\n public toMilliseconds(): number {\n return this.dateTime.toMillis();\n }\n\n /**\n * Creates a new instance of RockDateTime that represents the same point\n * in time as represented by the specified time zone offset.\n *\n * @param zone The time zone offset as a number or string such as \"UTC+4\".\n *\n * @returns A new RockDateTime instance that represents the specified time zone.\n */\n public toOffset(zone: number | string): RockDateTime {\n let dateTime: DateTime;\n\n if (typeof zone === \"number\") {\n dateTime = this.dateTime.setZone(FixedOffsetZone.instance(zone));\n }\n else {\n dateTime = this.dateTime.setZone(zone);\n }\n\n if (!dateTime.isValid) {\n throw \"Invalid time zone specified.\";\n }\n\n return new RockDateTime(dateTime);\n }\n\n /**\n * Formats this instance according to C# formatting rules.\n *\n * @param format The string that specifies the format to use.\n *\n * @returns A string representing this instance in the given format.\n */\n public toASPString(format: string): string {\n return formatAspDate(this, format);\n }\n\n /**\n * Creates a string representation of this instance in ISO8601 format.\n *\n * @returns An ISO8601 formatted string.\n */\n public toISOString(): string {\n return this.dateTime.toISO();\n }\n\n /**\n * Formats this instance using standard locale formatting rules to display\n * a date and time in the browsers specified locale.\n *\n * @param format The format to use when generating the string.\n *\n * @returns A string that represents the date and time in then specified format.\n */\n public toLocaleString(format: Intl.DateTimeFormatOptions): string {\n return this.dateTime.toLocaleString(format);\n }\n\n /**\n * Transforms the date into a human friendly elapsed time string.\n *\n * @example\n * // Returns \"21yrs\"\n * RockDateTime.fromParts(2000, 3, 4).toElapsedString();\n *\n * @returns A string that represents the amount of time that has elapsed.\n */\n public toElapsedString(currentDateTime?: RockDateTime): string {\n const msPerSecond = 1000;\n const msPerMinute= 1000 * 60;\n const msPerHour = 1000 * 60 * 60;\n const hoursPerDay = 24;\n const daysPerYear = 365;\n\n let start = new RockDateTime(this.dateTime);\n let end = currentDateTime ?? RockDateTime.now();\n let direction = \"Ago\";\n let totalMs = end.toMilliseconds() - start.toMilliseconds();\n\n if (totalMs < 0) {\n direction = \"From Now\";\n totalMs = Math.abs(totalMs);\n start = end;\n end = new RockDateTime(this.dateTime);\n }\n\n const totalSeconds = totalMs / msPerSecond;\n const totalMinutes = totalMs / msPerMinute;\n const totalHours = totalMs / msPerHour;\n const totalDays = totalHours / hoursPerDay;\n\n if (totalHours < 24) {\n if (totalSeconds < 2) {\n return `1 Second ${direction}`;\n }\n\n if (totalSeconds < 60) {\n return `${Math.floor(totalSeconds)} Seconds ${direction}`;\n }\n\n if (totalMinutes < 2) {\n return `1 Minute ${direction}`;\n }\n\n if (totalMinutes < 60) {\n return `${Math.floor(totalMinutes)} Minutes ${direction}`;\n }\n\n if (totalHours < 2) {\n return `1 Hour ${direction}`;\n }\n\n if (totalHours < 60) {\n return `${Math.floor(totalHours)} Hours ${direction}`;\n }\n }\n\n if (totalDays < 2) {\n return `1 Day ${direction}`;\n }\n\n if (totalDays < 31) {\n return `${Math.floor(totalDays)} Days ${direction}`;\n }\n\n const totalMonths = end.totalMonths(start);\n\n if (totalMonths <= 1) {\n return `1 Month ${direction}`;\n }\n\n if (totalMonths <= 18) {\n return `${Math.round(totalMonths)} Months ${direction}`;\n }\n\n const totalYears = Math.floor(totalDays / daysPerYear);\n\n if (totalYears <= 1) {\n return `1 Year ${direction}`;\n }\n\n return `${Math.round(totalYears)} Years ${direction}`;\n }\n\n /**\n * Formats this instance as a string that can be used in HTTP headers and\n * cookies.\n *\n * @returns A new string that conforms to RFC 1123\n */\n public toHTTPString(): string {\n return this.dateTime.toHTTP();\n }\n\n /**\n * Get the value of the date and time in a format that can be used in\n * comparisons.\n *\n * @returns A number that represents the date and time.\n */\n public valueOf(): number {\n return this.dateTime.valueOf();\n }\n\n /**\n * Creates a standard string representation of the date and time.\n *\n * @returns A string representation of the date and time.\n */\n public toString(): string {\n return this.toLocaleString(DateTimeFormat.DateTimeFull);\n }\n\n /**\n * Checks if this instance is equal to another RockDateTime instance. This\n * will return true if the two instances represent the same point in time,\n * even if they have been associated with different time zones. In other\n * words \"2021-09-08 12:00:00 Z\" == \"2021-09-08 14:00:00 UTC+2\".\n *\n * @param otherDateTime The other RockDateTime to be compared against.\n *\n * @returns True if the two instances represent the same point in time.\n */\n public isEqualTo(otherDateTime: RockDateTime): boolean {\n return this.dateTime.toMillis() === otherDateTime.dateTime.toMillis();\n }\n\n /**\n * Checks if this instance is later than another RockDateTime instance.\n *\n * @param otherDateTime The other RockDateTime to be compared against.\n *\n * @returns True if this instance represents a point in time that occurred after another point in time, regardless of time zone.\n */\n public isLaterThan(otherDateTime: RockDateTime): boolean {\n return this.dateTime.toMillis() > otherDateTime.dateTime.toMillis();\n }\n\n /**\n * Checks if this instance is earlier than another RockDateTime instance.\n *\n * @param otherDateTime The other RockDateTime to be compared against.\n *\n * @returns True if this instance represents a point in time that occurred before another point in time, regardless of time zone.\n */\n public isEarlierThan(otherDateTime: RockDateTime): boolean {\n return this.dateTime.toMillis() < otherDateTime.dateTime.toMillis();\n }\n\n /**\n * Calculates the elapsed time between this date and the reference date and\n * returns that difference in a human friendly way.\n *\n * @param otherDateTime The reference date and time. If not specified then 'now' is used.\n *\n * @returns A string that represents the elapsed time.\n */\n public humanizeElapsed(otherDateTime?: RockDateTime): string {\n otherDateTime = otherDateTime ?? RockDateTime.now();\n\n const totalSeconds = Math.floor((otherDateTime.dateTime.toMillis() - this.dateTime.toMillis()) / 1000);\n\n if (totalSeconds <= 1) {\n return \"right now\";\n }\n else if (totalSeconds < 60) { // 1 minute\n return `${totalSeconds} seconds ago`;\n }\n else if (totalSeconds < 3600) { // 1 hour\n return `${Math.floor(totalSeconds / 60)} minutes ago`;\n }\n else if (totalSeconds < 86400) { // 1 day\n return `${Math.floor(totalSeconds / 3600)} hours ago`;\n }\n else if (totalSeconds < 31536000) { // 1 year\n return `${Math.floor(totalSeconds / 86400)} days ago`;\n }\n else {\n return `${Math.floor(totalSeconds / 31536000)} years ago`;\n }\n }\n\n /**\n * The total number of months between the two dates.\n * @param otherDateTime The reference date and time.\n * @returns An int that represents the number of months between the two dates.\n */\n public totalMonths(otherDateTime: RockDateTime): number {\n return ((this.year * 12) + this.month) - ((otherDateTime.year * 12) + otherDateTime.month);\n }\n\n // #endregion\n}\n","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n\nimport mitt, { Emitter } from \"mitt\";\n\n// NOTE: Much of the logic for this was taken from VSCode's MIT licensed version:\n// https://github.com/microsoft/vscode/blob/342394d1e7d43d3324dc2ede1d634cffd52ba159/src/vs/base/common/cancellation.ts\n\n/**\n * A cancellation token can be used to instruct some operation to run but abort\n * if a certain condition is met.\n */\nexport interface ICancellationToken {\n /**\n * A flag signalling is cancellation has been requested.\n */\n readonly isCancellationRequested: boolean;\n\n /**\n * Registers a listener for when cancellation has been requested. This event\n * only ever fires `once` as cancellation can only happen once. Listeners\n * that are registered after cancellation will be called (next event loop run),\n * but also only once.\n *\n * @param listener The function to be called when the token has been cancelled.\n */\n onCancellationRequested(listener: () => void): void;\n}\n\nfunction shortcutCancelledEvent(listener: () => void): void {\n window.setTimeout(listener, 0);\n}\n\n/**\n * Determines if something is a cancellation token.\n *\n * @param thing The thing to be checked to see if it is a cancellation token.\n *\n * @returns true if the @{link thing} is a cancellation token, otherwise false.\n */\nexport function isCancellationToken(thing: unknown): thing is ICancellationToken {\n if (thing === CancellationTokenNone || thing === CancellationTokenCancelled) {\n return true;\n }\n if (thing instanceof MutableToken) {\n return true;\n }\n if (!thing || typeof thing !== \"object\") {\n return false;\n }\n return typeof (thing as ICancellationToken).isCancellationRequested === \"boolean\"\n && typeof (thing as ICancellationToken).onCancellationRequested === \"function\";\n}\n\n/**\n * A cancellation token that will never be in a cancelled state.\n */\nexport const CancellationTokenNone = Object.freeze({\n isCancellationRequested: false,\n onCancellationRequested() {\n // Intentionally blank.\n }\n});\n\n/**\n * A cancellation token that is already in a cancelled state.\n */\nexport const CancellationTokenCancelled = Object.freeze({\n isCancellationRequested: true,\n onCancellationRequested: shortcutCancelledEvent\n});\n\n/**\n * Internal implementation of a cancellation token that starts initially as\n * active but can later be switched to a cancelled state.\n */\nclass MutableToken implements ICancellationToken {\n private isCancelled: boolean = false;\n private emitter: Emitter> | null = null;\n\n /**\n * Cancels the token and fires any registered event listeners.\n */\n public cancel(): void {\n if (!this.isCancelled) {\n this.isCancelled = true;\n if (this.emitter) {\n this.emitter.emit(\"cancel\", undefined);\n this.emitter = null;\n }\n }\n }\n\n // #region ICancellationToken implementation\n\n get isCancellationRequested(): boolean {\n return this.isCancelled;\n }\n\n onCancellationRequested(listener: () => void): void {\n if (this.isCancelled) {\n return shortcutCancelledEvent(listener);\n }\n\n if (!this.emitter) {\n this.emitter = mitt();\n }\n\n this.emitter.on(\"cancel\", listener);\n }\n\n // #endregion\n}\n\n/**\n * Creates a source instance that can be used to trigger a cancellation\n * token into the cancelled state.\n */\nexport class CancellationTokenSource {\n /** The token that can be passed to functions. */\n private internalToken?: ICancellationToken = undefined;\n\n /**\n * Creates a new instance of {@link CancellationTokenSource}.\n *\n * @param parent The parent cancellation token that will also cancel this source.\n */\n constructor(parent?: ICancellationToken) {\n if (parent) {\n parent.onCancellationRequested(() => this.cancel());\n }\n }\n\n /**\n * The cancellation token that can be used to determine when the task\n * should be cancelled.\n */\n get token(): ICancellationToken {\n if (!this.internalToken) {\n // be lazy and create the token only when\n // actually needed\n this.internalToken = new MutableToken();\n }\n\n return this.internalToken;\n }\n\n /**\n * Moves the token into a cancelled state.\n */\n cancel(): void {\n if (!this.internalToken) {\n // Save an object creation by returning the default cancelled\n // token when cancellation happens before someone asks for the\n // token.\n this.internalToken = CancellationTokenCancelled;\n\n }\n else if (this.internalToken instanceof MutableToken) {\n // Actually cancel the existing token.\n this.internalToken.cancel();\n }\n }\n}\n","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n//\n\nimport { CancellationTokenSource, ICancellationToken } from \"./cancellation\";\n\n/**\n * Compares two values for equality by performing deep nested comparisons\n * if the values are objects or arrays.\n *\n * @param a The first value to compare.\n * @param b The second value to compare.\n * @param strict True if strict comparision is required (meaning 1 would not equal \"1\").\n *\n * @returns True if the two values are equal to each other.\n */\nexport function deepEqual(a: unknown, b: unknown, strict: boolean): boolean {\n // Catches everything but objects.\n if (strict && a === b) {\n return true;\n }\n else if (!strict && a == b) {\n return true;\n }\n\n // NaN never equals another NaN, but functionally they are the same.\n if (typeof a === \"number\" && typeof b === \"number\" && isNaN(a) && isNaN(b)) {\n return true;\n }\n\n // Remaining value types must both be of type object\n if (a && b && typeof a === \"object\" && typeof b === \"object\") {\n // Array status must match.\n if (Array.isArray(a) !== Array.isArray(b)) {\n return false;\n }\n\n if (Array.isArray(a) && Array.isArray(b)) {\n // Array lengths must match.\n if (a.length !== b.length) {\n return false;\n }\n\n // Each element in the array must match.\n for (let i = 0; i < a.length; i++) {\n if (!deepEqual(a[i], b[i], strict)) {\n return false;\n }\n }\n\n return true;\n }\n else {\n // NOTE: There are a few edge cases not accounted for here, but they\n // are rare and unusual:\n // Map, Set, ArrayBuffer, RegExp\n\n // The objects must be of the same \"object type\".\n if (a.constructor !== b.constructor) {\n return false;\n }\n\n // Get all the key/value pairs of each object and sort them so they\n // are in the same order as each other.\n const aEntries = Object.entries(a).sort((a, b) => a[0] < b[0] ? -1 : (a[0] > b[0] ? 1 : 0));\n const bEntries = Object.entries(b).sort((a, b) => a[0] < b[0] ? -1 : (a[0] > b[0] ? 1 : 0));\n\n // Key/value count must be identical.\n if (aEntries.length !== bEntries.length) {\n return false;\n }\n\n for (let i = 0; i < aEntries.length; i++) {\n const aEntry = aEntries[i];\n const bEntry = bEntries[i];\n\n // Ensure the keys are equal, must always be strict.\n if (!deepEqual(aEntry[0], bEntry[0], true)) {\n return false;\n }\n\n // Ensure the values are equal.\n if (!deepEqual(aEntry[1], bEntry[1], strict)) {\n return false;\n }\n }\n\n return true;\n }\n }\n\n return false;\n}\n\n\n/**\n * Debounces the function so it will only be called once during the specified\n * delay period. The returned function should be called to trigger the original\n * function that is to be debounced.\n *\n * @param fn The function to be called once per delay period.\n * @param delay The period in milliseconds. If the returned function is called\n * more than once during this period then fn will only be executed once for\n * the period. If not specified then it defaults to 250ms.\n * @param eager If true then the fn function will be called immediately and\n * then any subsequent calls will be debounced.\n *\n * @returns A function to be called when fn should be executed.\n */\nexport function debounce(fn: (() => void), delay: number = 250, eager: boolean = false): (() => void) {\n let timeout: NodeJS.Timeout | null = null;\n\n return (): void => {\n if (timeout) {\n clearTimeout(timeout);\n }\n else if (eager) {\n // If there was no previous timeout and we are configured for\n // eager calls, then execute now.\n fn();\n\n // An eager call should not result in a final debounce call.\n timeout = setTimeout(() => timeout = null, delay);\n\n return;\n }\n\n // If we had a previous timeout or we are not set for eager calls\n // then set a timeout to initiate the function after the delay.\n timeout = setTimeout(() => {\n timeout = null;\n fn();\n }, delay);\n };\n}\n\n/**\n * Options for debounceAsync function\n */\ntype DebounceAsyncOptions = {\n /**\n * The period in milliseconds. If the returned function is called more than\n * once during this period, then `fn` will only be executed once for the\n * period.\n * @default 250\n */\n delay?: number;\n\n /**\n * If `true`, then the `fn` function will be called immediately on the first\n * call, and any subsequent calls will be debounced.\n * @default false\n */\n eager?: boolean;\n};\n\n/**\n * Debounces the function so it will only be called once during the specified\n * delay period. The returned function should be called to trigger the original\n * function that is to be debounced.\n *\n * **Note:** Due to the asynchronous nature of JavaScript and how promises work,\n * `fn` may be invoked multiple times before previous invocations have completed.\n * To ensure that only the latest invocation proceeds and to prevent stale data,\n * you should check `cancellationToken.isCancellationRequested` at appropriate\n * points within your `fn` implementation—ideally after you `await` data from the\n * server. If cancellation is requested, `fn` should promptly abort execution.\n *\n * @param fn The function to be called once per delay period.\n * @param options An optional object specifying debounce options.\n *\n * @returns A function to be called when `fn` should be executed. This function\n * accepts an optional `parentCancellationToken` that, when canceled, will also\n * cancel the execution of `fn`.\n */\nexport function debounceAsync(\n fn: ((cancellationToken?: ICancellationToken) => PromiseLike),\n options?: DebounceAsyncOptions\n): ((parentCancellationToken?: ICancellationToken) => Promise) {\n const delay = options?.delay ?? 250;\n const eager = options?.eager ?? false;\n\n let timeout: NodeJS.Timeout | null = null;\n let source: CancellationTokenSource | null = null;\n let isEagerExecutionInProgress = false;\n\n return async (parentCancellationToken?: ICancellationToken): Promise => {\n // Always cancel any ongoing execution of fn.\n source?.cancel();\n\n if (timeout) {\n clearTimeout(timeout);\n timeout = null;\n }\n else if (eager && !isEagerExecutionInProgress) {\n // Execute immediately on the first call.\n isEagerExecutionInProgress = true;\n source = new CancellationTokenSource(parentCancellationToken);\n\n // Set the timeout before awaiting fn.\n timeout = setTimeout(() => {\n timeout = null;\n isEagerExecutionInProgress = false;\n }, delay);\n\n try {\n await fn(source.token);\n }\n catch (e) {\n console.error(e || \"Unknown error while debouncing async function call.\");\n throw e;\n }\n\n return;\n }\n\n // Schedule the function to run after the delay.\n source = new CancellationTokenSource(parentCancellationToken);\n const cts = source;\n timeout = setTimeout(async () => {\n try {\n await fn(cts.token);\n }\n catch (e) {\n console.error(e || \"Unknown error while debouncing async function call.\");\n throw e;\n }\n\n timeout = null;\n isEagerExecutionInProgress = false;\n }, delay);\n };\n}\n","import { Guid } from \"@Obsidian/Types\";\nimport { BlockBeginEditData, BlockEndEditData, BrowserBusCallback, BrowserBusOptions, Message, QueryStringChangedData } from \"@Obsidian/Types/Utility/browserBus\";\nimport { areEqual } from \"./guid\";\n\n/*\n * READ THIS BEFORE MAKING ANY CHANGES TO THE BUS.\n *\n * OVERVIEW\n *\n * The browser bus is a basic pubsub interface within a single page. If you\n * publish a message to one instance of the bus it will be available to any\n * other instance on the same page. This uses document.addEventListener()\n * and document.dispatchEvent() with a single custom event name of `rockMessage`.\n *\n * The browser bus will not communicate with other browsers on the same page or\n * even other tabs within the same browser.\n *\n * For full documentation, see the gitbook developer documentation.\n *\n * FRAMEWORK MESSAGES\n *\n * All \"framework\" messages should have a type defined in\n * @Obsidian/Types/Utility/browserBus that specify the data type expected. If\n * no data type is expected than `void` can be used as the type. Message data\n * should always be an object rather than a primitive. This allows us to add\n * additional values without it being a breaking change to existing code.\n *\n * Additionally, all framework messages should have their name defined in either\n * the PageMessages object or BlockMessages object. This is for uniformity so it\n * is easier for core code and plugins to subscribe to these messages and know\n * they got the right message name.\n *\n * SUBSCRIBE OVERLOADS\n *\n * When adding new framework messages, be sure to add overloads to the\n * subscribe, subscribeToBlock and subscribeToBlockType functions for that\n * message name and data type. This compiles away to nothing but provides a\n * much better TypeScript experience.\n */\n\n\n/**\n * Framework messages that will be sent for pages.\n */\nexport const PageMessages = {\n /**\n * Sent when the query string is changed outside the context of a page load.\n */\n QueryStringChanged: \"page.core.queryStringChanged\"\n} as const;\n\n/**\n * Framework messages that will be sent for blocks.\n */\nexport const BlockMessages = {\n /**\n * Sent just before a block switches into edit mode.\n */\n BeginEdit: \"block.core.beginEdit\",\n\n /**\n * Sent just after a block switches out of edit mode.\n */\n EndEdit: \"block.core.endEdit\",\n} as const;\n\n/**\n * Gets an object that will provide access to the browser bus. This bus will\n * allow different code on the page to send and receive messages betwen each\n * other as well as plain JavaScript. This bus does not cross page boundaries.\n *\n * Meaning, if you publish a message in one tab it will not show up in another\n * tab in the same (or a different) browser. Neither will messages magically\n * persist across page loads.\n *\n * If you call this method you are responsible for calling the {@link BrowserBus.dispose}\n * function when you are done with the bus. If you do not then your component\n * will probably never be garbage collected and your subscribed event handlers\n * will continue to be called.\n *\n * @param options Custom options to construct the {@link BrowserBus} object with. This should normally not be needed.\n *\n * @returns The object that provides access to the browser bus.\n */\nexport function useBrowserBus(options?: BrowserBusOptions): BrowserBus {\n return new BrowserBus(options ?? {});\n}\n\n// #region Internal Types\n\n/**\n * Internal message handler state that includes the filters used to decide\n * if the callback is valid for the message.\n */\ntype MessageHandler = {\n /** If not nullish messages must match this message name. */\n name?: string;\n\n /** If not nullish then messages must be from this block type. */\n blockType?: Guid;\n\n /** If not nullish them messages must be from this block instance. */\n block?: Guid;\n\n /** The callback that will be called. */\n callback: BrowserBusCallback;\n};\n\n// #endregion\n\n// #region Internal Implementation\n\n/** This is the JavaScript event name we use with dispatchEvent(). */\nconst customDomEventName = \"rockMessage\";\n\n/**\n * The main browser bus implementation. This uses a shared method to publish\n * and subscribe to messages such that if you create two BrowserBus instances on\n * the same page they will still be able to talk to each other.\n *\n * However, they will not be able to talk to instances on other pages such as\n * in other browser tabs.\n */\nexport class BrowserBus {\n /** The registered handlers that will potentially be invoked. */\n private handlers: MessageHandler[] = [];\n\n /** The options we were created with. */\n private options: BrowserBusOptions;\n\n /** The event listener. Used so we can remove the listener later. */\n private eventListener: (e: Event) => void;\n\n /**\n * Creates a new instance of the bus and prepares it to receive messages.\n *\n * This should be considered an internal constructor and not used by plugins.\n *\n * @param options The options that describe how this instance should operate.\n */\n constructor(options: BrowserBusOptions) {\n this.options = { ...options };\n\n this.eventListener = e => this.onEvent(e);\n document.addEventListener(customDomEventName, this.eventListener);\n }\n\n // #region Private Functions\n\n /**\n * Called when an event is received from the document listener.\n *\n * @param event The low level JavaScript even that was received.\n */\n private onEvent(event: Event): void {\n if (!(event instanceof CustomEvent)) {\n return;\n }\n\n let message = event.detail as Message;\n\n // Discard the message if it is not valid.\n if (!message.name) {\n return;\n }\n\n // If we got a message without a timestamp, it probably came from\n // plain JavaScript, so set it to 0.\n if (typeof message.timestamp === \"undefined\") {\n message = { ...message, timestamp: 0 };\n }\n\n this.onMessage(message);\n }\n\n /**\n * Called when a browser bus message is received from the bus.\n *\n * @param message The message that was received.\n */\n private onMessage(message: Message): void {\n // Make a copy of the handlers in case our list of handlers if modified\n // inside a handler.\n const handlers = [...this.handlers];\n\n for (const handler of handlers) {\n try {\n // Perform all the filtering. We could do this all in one\n // line but this is easier to read and understand.\n if (handler.name && handler.name !== message.name) {\n continue;\n }\n\n if (handler.blockType && !areEqual(handler.blockType, message.blockType)) {\n continue;\n }\n\n if (handler.block && !areEqual(handler.block, message.block)) {\n continue;\n }\n\n // All filters passed, execute the callback.\n handler.callback(message);\n }\n catch (e) {\n // Catch the error and display it so other handlers will still\n // be checked and called.\n console.error(e);\n }\n }\n }\n\n // #endregion\n\n // #region Public Functions\n\n /**\n * Frees up any resources used by this browser bus instance.\n */\n public dispose(): void {\n document.removeEventListener(customDomEventName, this.eventListener);\n this.handlers.splice(0, this.handlers.length);\n }\n\n /**\n * Publishes a named message without any data.\n *\n * @param messageName The name of the message to publish.\n */\n public publish(messageName: string): void;\n\n /**\n * Publishes a named message with some custom data.\n *\n * @param messageName The name of the message to publish.\n * @param data The custom data to include with the message.\n */\n public publish(messageName: string, data: unknown): void;\n\n /**\n * Publishes a named message with some custom data.\n *\n * @param messageName The name of the message to publish.\n * @param data The custom data to include with the message.\n */\n public publish(messageName: string, data?: unknown): void {\n this.publishMessage({\n name: messageName,\n timestamp: Date.now(),\n blockType: this.options.blockType,\n block: this.options.block,\n data\n });\n }\n\n /**\n * Publishes a message to the browser bus. No changes are made to the\n * message object.\n *\n * Do not use this message to publish a block message unless you have\n * manually filled in the {@link Message.blockType} and\n * {@link Message.block} properties.\n *\n * @param message The message to publish.\n */\n public publishMessage(message: Message): void {\n const event = new CustomEvent(customDomEventName, {\n detail: message\n });\n\n document.dispatchEvent(event);\n }\n\n // #endregion\n\n // #region subscribe()\n\n /**\n * Subscribes to the named message from any source.\n *\n * @param messageName The name of the message to subscribe to.\n * @param callback The callback to invoke when the message is received.\n */\n public subscribe(messageName: string, callback: BrowserBusCallback): void;\n\n /**\n * Subscribes to the named message from any source.\n *\n * @param messageName The name of the message to subscribe to.\n * @param callback The callback to invoke when the message is received.\n */\n public subscribe(messageName: \"page.core.queryStringChanged\", callback: BrowserBusCallback): void;\n\n /**\n * Subscribes to the named message from any source.\n *\n * @param messageName The name of the message to subscribe to.\n * @param callback The callback to invoke when the message is received.\n */\n public subscribe(messageName: \"block.core.beginEdit\", callback: BrowserBusCallback): void;\n\n /**\n * Subscribes to the named message from any source.\n *\n * @param messageName The name of the message to subscribe to.\n * @param callback The callback to invoke when the message is received.\n */\n public subscribe(messageName: \"block.core.endEdit\", callback: BrowserBusCallback): void;\n\n /**\n * Subscribes to any message that is sent.\n *\n * @param callback The callback to invoke when the message is received.\n */\n public subscribe(callback: BrowserBusCallback): void;\n\n /**\n * Subscribes to messages from any source.\n *\n * @param messageNameOrCallback The name of the message to subscribe to or the callback.\n * @param callback The callback to invoke when the message is received.\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n public subscribe(messageNameOrCallback: string | BrowserBusCallback, callback?: BrowserBusCallback): void {\n let name: string | undefined;\n\n if (typeof messageNameOrCallback === \"string\") {\n name = messageNameOrCallback;\n }\n else {\n name = undefined;\n callback = messageNameOrCallback;\n }\n\n if (!callback) {\n return;\n }\n\n this.handlers.push({\n name,\n callback\n });\n }\n\n // #endregion\n\n // #region subscribeToBlockType()\n\n /**\n * Subscribes to the named message from any block instance with a matching\n * block type identifier.\n *\n * @param messageName The name of the message to subscribe to.\n * @param blockType The identifier of the block type.\n * @param callback The callback to invoke when the message is received.\n */\n public subscribeToBlockType(messageName: string, blockType: Guid, callback: BrowserBusCallback): void;\n\n /**\n * Subscribes to the named message from any block instance with a matching\n * block type identifier.\n *\n * @param messageName The name of the message to subscribe to.\n * @param blockType The identifier of the block type.\n * @param callback The callback to invoke when the message is received.\n */\n public subscribeToBlockType(messageName: \"block.core.beginEdit\", blockType: Guid, callback: BrowserBusCallback): void;\n\n /**\n * Subscribes to the named message from any block instance with a matching\n * block type identifier.\n *\n * @param messageName The name of the message to subscribe to.\n * @param blockType The identifier of the block type.\n * @param callback The callback to invoke when the message is received.\n */\n public subscribeToBlockType(messageName: \"block.core.endEdit\", blockType: Guid, callback: BrowserBusCallback): void;\n\n /**\n * Subscribes to any message that is sent from any block instance with a\n * matching block type identifier.\n *\n * @param blockType The identifier of the block type.\n * @param callback The callback to invoke when the message is received.\n */\n public subscribeToBlockType(blockType: Guid, callback: BrowserBusCallback): void;\n\n /**\n * Subscribes to messages from any block instance with a matching block\n * type identifier.\n *\n * @param messageNameOrBlockType The name of the message to subscribe to or the block type.\n * @param blockTypeOrCallback The block type or the callback function.\n * @param callback The callback to invoke when the message is received.\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n public subscribeToBlockType(messageNameOrBlockType: string | Guid, blockTypeOrCallback: Guid | BrowserBusCallback, callback?: BrowserBusCallback): void {\n let name: string | undefined;\n let blockType: Guid;\n\n if (typeof blockTypeOrCallback === \"string\") {\n name = messageNameOrBlockType;\n blockType = blockTypeOrCallback;\n }\n else {\n blockType = messageNameOrBlockType;\n callback = blockTypeOrCallback;\n }\n\n if (!blockType || !callback) {\n return;\n }\n\n this.handlers.push({\n name,\n blockType,\n callback\n });\n }\n\n // #endregion\n\n // #region subscribeToBlock()\n\n /**\n * Subscribes to the named message from a single block instance.\n *\n * @param messageName The name of the message to subscribe to.\n * @param block The identifier of the block.\n * @param callback The callback to invoke when the message is received.\n */\n public subscribeToBlock(messageName: string, block: Guid, callback: BrowserBusCallback): void;\n\n /**\n * Subscribes to the named message from a single block instance.\n *\n * @param messageName The name of the message to subscribe to.\n * @param block The identifier of the block.\n * @param callback The callback to invoke when the message is received.\n */\n public subscribeToBlock(messageName: \"block.core.beginEdit\", block: Guid, callback: BrowserBusCallback): void;\n\n /**\n * Subscribes to the named message from a single block instance.\n *\n * @param messageName The name of the message to subscribe to.\n * @param block The identifier of the block.\n * @param callback The callback to invoke when the message is received.\n */\n public subscribeToBlock(messageName: \"block.core.endEdit\", block: Guid, callback: BrowserBusCallback): void;\n\n /**\n * Subscribes to any message that is sent from a single block instance.\n *\n * @param block The identifier of the block.\n * @param callback The callback to invoke when the message is received.\n */\n public subscribeToBlock(block: Guid, callback: BrowserBusCallback): void;\n\n /**\n * Subscribes to messages from a single block instance.\n *\n * @param messageNameOrBlock The name of the message to subscribe to or the block.\n * @param blockOrCallback The block or the callback function.\n * @param callback The callback to invoke when the message is received.\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n public subscribeToBlock(messageNameOrBlock: string | Guid, blockOrCallback: Guid | BrowserBusCallback, callback?: BrowserBusCallback): void {\n let name: string | undefined;\n let block: Guid;\n\n if (typeof blockOrCallback === \"string\") {\n name = messageNameOrBlock;\n block = blockOrCallback;\n }\n else {\n block = messageNameOrBlock;\n callback = blockOrCallback;\n }\n\n if (!block || !callback) {\n return;\n }\n\n this.handlers.push({\n name,\n block,\n callback\n });\n }\n\n // #endregion\n}\n\n// #endregion\n","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n//\n\nimport { BlockEvent, InvokeBlockActionFunc, SecurityGrant } from \"@Obsidian/Types/Utility/block\";\nimport { IBlockPersonPreferencesProvider, IPersonPreferenceCollection } from \"@Obsidian/Types/Core/personPreferences\";\nimport { ExtendedRef } from \"@Obsidian/Types/Utility/component\";\nimport { DetailBlockBox } from \"@Obsidian/ViewModels/Blocks/detailBlockBox\";\nimport { inject, provide, Ref, ref, watch } from \"vue\";\nimport { RockDateTime } from \"./rockDateTime\";\nimport { Guid } from \"@Obsidian/Types\";\nimport { HttpBodyData, HttpPostFunc, HttpResult } from \"@Obsidian/Types/Utility/http\";\nimport { BlockActionContextBag } from \"@Obsidian/ViewModels/Blocks/blockActionContextBag\";\nimport { ValidPropertiesBox } from \"@Obsidian/ViewModels/Utility/validPropertiesBox\";\nimport { IEntity } from \"@Obsidian/ViewModels/entity\";\nimport { debounce } from \"./util\";\nimport { BrowserBus, useBrowserBus } from \"./browserBus\";\n\nconst blockReloadSymbol = Symbol();\nconst configurationValuesChangedSymbol = Symbol();\nconst staticContentSymbol = Symbol(\"static-content\");\nconst blockBrowserBusSymbol = Symbol(\"block-browser-bus\");\n\n// TODO: Change these to use symbols\n\n/**\n * Maps the block configuration values to the expected type.\n *\n * @returns The configuration values for the block.\n */\nexport function useConfigurationValues(): T {\n const result = inject[>(\"configurationValues\");\n\n if (result === undefined) {\n throw \"Attempted to access block configuration outside of a RockBlock.\";\n }\n\n return result.value;\n}\n\n/**\n * Gets the function that will be used to invoke block actions.\n *\n * @returns An instance of @see {@link InvokeBlockActionFunc}.\n */\nexport function useInvokeBlockAction(): InvokeBlockActionFunc {\n const result = inject(\"invokeBlockAction\");\n\n if (result === undefined) {\n throw \"Attempted to access block action invocation outside of a RockBlock.\";\n }\n\n return result;\n}\n\n/**\n * Gets the function that will return the URL for a block action.\n *\n * @returns A function that can be called to determine the URL for a block action.\n */\nexport function useBlockActionUrl(): (actionName: string) => string {\n const result = inject<(actionName: string) => string>(\"blockActionUrl\");\n\n if (result === undefined) {\n throw \"Attempted to access block action URL outside of a RockBlock.\";\n }\n\n return result;\n}\n\n/**\n * Creates a function that can be provided to the block that allows calling\n * block actions.\n *\n * @private This should not be used by plugins.\n *\n * @param post The function to handle the post operation.\n * @param pageGuid The unique identifier of the page.\n * @param blockGuid The unique identifier of the block.\n * @param pageParameters The parameters to include with the block action calls.\n *\n * @returns A function that can be used to provide the invoke block action.\n */\nexport function createInvokeBlockAction(post: HttpPostFunc, pageGuid: Guid, blockGuid: Guid, pageParameters: Record, interactionGuid: Guid): InvokeBlockActionFunc {\n async function invokeBlockAction(actionName: string, data: HttpBodyData | undefined = undefined, actionContext: BlockActionContextBag | undefined = undefined): Promise> {\n let context: BlockActionContextBag = {};\n\n if (actionContext) {\n context = { ...actionContext };\n }\n\n context.pageParameters = pageParameters;\n context.interactionGuid = interactionGuid;\n\n return await post(`/api/v2/BlockActions/${pageGuid}/${blockGuid}/${actionName}`, undefined, {\n __context: context,\n ...data\n });\n }\n\n return invokeBlockAction;\n}\n\n/**\n * Provides the reload block callback function for a block. This is an internal\n * method and should not be used by plugins.\n *\n * @param callback The callback that will be called when a block wants to reload itself.\n */\nexport function provideReloadBlock(callback: () => void): void {\n provide(blockReloadSymbol, callback);\n}\n\n/**\n * Gets a function that can be called when a block wants to reload itself.\n *\n * @returns A function that will cause the block component to be reloaded.\n */\nexport function useReloadBlock(): () => void {\n return inject<() => void>(blockReloadSymbol, () => {\n // Intentionally blank, do nothing by default.\n });\n}\n\n/**\n * Provides the data for a block to be notified when its configuration values\n * have changed. This is an internal method and should not be used by plugins.\n *\n * @returns An object with an invoke and reset function.\n */\nexport function provideConfigurationValuesChanged(): { invoke: () => void, reset: () => void } {\n const callbacks: (() => void)[] = [];\n\n provide(configurationValuesChangedSymbol, callbacks);\n\n return {\n invoke: (): void => {\n for (const c of callbacks) {\n c();\n }\n },\n\n reset: (): void => {\n callbacks.splice(0, callbacks.length);\n }\n };\n}\n\n/**\n * Registered a function to be called when the block configuration values have\n * changed.\n *\n * @param callback The function to be called when the configuration values have changed.\n */\nexport function onConfigurationValuesChanged(callback: () => void): void {\n const callbacks = inject<(() => void)[]>(configurationValuesChangedSymbol);\n\n if (callbacks !== undefined) {\n callbacks.push(callback);\n }\n}\n\n/**\n * Provides the static content that the block provided on the server.\n *\n * @param content The static content from the server.\n */\nexport function provideStaticContent(content: Ref): void {\n provide(staticContentSymbol, content);\n}\n\n/**\n * Gets the static content that was provided by the block on the server.\n *\n * @returns A string of HTML content or undefined.\n */\nexport function useStaticContent(): Node[] {\n const content = inject][>(staticContentSymbol);\n\n if (!content) {\n return [];\n }\n\n return content.value;\n}\n\n/**\n * Provides the browser bus configured to publish messages for the current\n * block.\n *\n * @param bus The browser bus.\n */\nexport function provideBlockBrowserBus(bus: BrowserBus): void {\n provide(blockBrowserBusSymbol, bus);\n}\n\n/**\n * Gets the browser bus configured for use by the current block. If available\n * this will be properly configured to publish messages with the correct block\n * and block type. If this is called outside the context of a block then a\n * generic use {@link BrowserBus} will be returned.\n *\n * @returns An instance of {@link BrowserBus}.\n */\nexport function useBlockBrowserBus(): BrowserBus {\n return inject(blockBrowserBusSymbol, () => useBrowserBus(), true);\n}\n\n\n/**\n * A type that returns the keys of a child property.\n */\ntype ChildKeys, PropertyName extends string> = keyof NonNullable & string;\n\n/**\n * A valid properties box that uses the specified name for the content bag.\n */\ntype ValidPropertiesSettingsBox = {\n validProperties?: string[] | null;\n} & {\n settings?: Record | null;\n};\n\n/**\n * Sets the a value for a custom settings box. This will set the value and then\n * add the property name to the list of valid properties.\n *\n * @param box The box whose custom setting value will be set.\n * @param propertyName The name of the custom setting property to set.\n * @param value The new value of the custom setting.\n */\nexport function setCustomSettingsBoxValue, K extends ChildKeys>(box: T, propertyName: K, value: S[K]): void {\n if (!box.settings) {\n box.settings = {} as Record;\n }\n\n box.settings[propertyName] = value;\n\n if (!box.validProperties) {\n box.validProperties = [];\n }\n\n if (!box.validProperties.includes(propertyName)) {\n box.validProperties.push(propertyName);\n }\n}\n\n/**\n * Sets the a value for a property box. This will set the value and then\n * add the property name to the list of valid properties.\n *\n * @param box The box whose property value will be set.\n * @param propertyName The name of the property on the bag to set.\n * @param value The new value of the property.\n */\nexport function setPropertiesBoxValue, K extends keyof T & string>(box: ValidPropertiesBox, propertyName: K, value: T[K]): void {\n if (!box.bag) {\n box.bag = {} as Record as T;\n }\n\n box.bag[propertyName] = value;\n\n if (!box.validProperties) {\n box.validProperties = [];\n }\n\n if (!box.validProperties.some(p => p.toLowerCase() === propertyName.toLowerCase())) {\n box.validProperties.push(propertyName);\n }\n}\n\n/**\n * Dispatches a block event to the document.\n *\n * @deprecated Do not use this function anymore, it will be removed in the future.\n * Use the BrowserBus instead.\n *\n * @param eventName The name of the event to be dispatched.\n * @param eventData The custom data to be attached to the event.\n *\n * @returns true if preventDefault() was called on the event, otherwise false.\n */\nexport function dispatchBlockEvent(eventName: string, blockGuid: Guid, eventData?: unknown): boolean {\n const ev = new CustomEvent(eventName, {\n cancelable: true,\n detail: {\n guid: blockGuid,\n data: eventData\n }\n });\n\n return document.dispatchEvent(ev);\n}\n\n/**\n * Tests if the given event is a custom block event. This does not ensure\n * that the event data is the correct type, only the event itself.\n *\n * @param event The event to be tested.\n *\n * @returns true if the event is a block event.\n */\nexport function isBlockEvent(event: Event): event is CustomEvent> {\n return \"guid\" in event && \"data\" in event;\n}\n\n// #region Entity Detail Blocks\n\nconst entityTypeNameSymbol = Symbol(\"EntityTypeName\");\nconst entityTypeGuidSymbol = Symbol(\"EntityTypeGuid\");\n\ntype UseEntityDetailBlockOptions = {\n /** The block configuration. */\n blockConfig: Record;\n\n /**\n * The entity that will be used by the block, this will cause the\n * onPropertyChanged logic to be generated.\n */\n entity?: Ref>;\n};\n\ntype UseEntityDetailBlockResult = {\n /** The onPropertyChanged handler for the edit panel. */\n onPropertyChanged?(propertyName: string): void;\n};\n\n/**\n * Performs any framework-level initialization of an entity detail block.\n *\n * @param options The options to use when initializing the detail block logic.\n *\n * @returns An object that contains information which can be used by the block.\n */\nexport function useEntityDetailBlock(options: UseEntityDetailBlockOptions): UseEntityDetailBlockResult {\n const securityGrant = getSecurityGrant(options.blockConfig.securityGrantToken as string);\n\n provideSecurityGrant(securityGrant);\n\n if (options.blockConfig.entityTypeName) {\n provideEntityTypeName(options.blockConfig.entityTypeName as string);\n }\n\n if (options.blockConfig.entityTypeGuid) {\n provideEntityTypeGuid(options.blockConfig.entityTypeGuid as Guid);\n }\n\n const entity = options.entity;\n\n const result: Record = {};\n\n if (entity) {\n const invokeBlockAction = useInvokeBlockAction();\n const refreshAttributesDebounce = debounce(() => refreshEntityDetailAttributes(entity, invokeBlockAction), undefined, true);\n\n result.onPropertyChanged = (propertyName: string): void => {\n // If we don't have any qualified attribute properties or this property\n // is not one of them then do nothing.\n if (!options.blockConfig.qualifiedAttributeProperties || !(options.blockConfig.qualifiedAttributeProperties as string[]).some(n => n.toLowerCase() === propertyName.toLowerCase())) {\n return;\n }\n\n refreshAttributesDebounce();\n };\n }\n\n return result;\n}\n\n/**\n * Provides the entity type name to child components.\n *\n * @param name The entity type name in PascalCase, such as `GroupMember`.\n */\nexport function provideEntityTypeName(name: string): void {\n provide(entityTypeNameSymbol, name);\n}\n\n/**\n * Gets the entity type name provided from a parent component.\n *\n * @returns The entity type name in PascalCase, such as `GroupMember` or undefined.\n */\nexport function useEntityTypeName(): string | undefined {\n return inject(entityTypeNameSymbol, undefined);\n}\n\n/**\n * Provides the entity type unique identifier to child components.\n *\n * @param guid The entity type unique identifier.\n */\nexport function provideEntityTypeGuid(guid: Guid): void {\n provide(entityTypeGuidSymbol, guid);\n}\n\n/**\n * Gets the entity type unique identifier provided from a parent component.\n *\n * @returns The entity type unique identifier or undefined.\n */\nexport function useEntityTypeGuid(): Guid | undefined {\n return inject(entityTypeGuidSymbol, undefined);\n}\n\n// #endregion\n\n// #region Security Grants\n\nconst securityGrantSymbol = Symbol();\n\n/**\n * Use a security grant token value provided by the server. This returns a reference\n * to the actual value and will automatically handle renewing the token and updating\n * the value. This function is meant to be used by blocks. Controls should use the\n * useSecurityGrant() function instead.\n *\n * @param token The token provided by the server.\n *\n * @returns A reference to the security grant that will be updated automatically when it has been renewed.\n */\nexport function getSecurityGrant(token: string | null | undefined): SecurityGrant {\n // Use || so that an empty string gets converted to null.\n const tokenRef = ref(token || null);\n const invokeBlockAction = useInvokeBlockAction();\n let renewalTimeout: NodeJS.Timeout | null = null;\n\n // Internal function to renew the token and re-schedule renewal.\n const renewToken = async (): Promise => {\n const result = await invokeBlockAction(\"RenewSecurityGrantToken\");\n\n if (result.isSuccess && result.data) {\n tokenRef.value = result.data;\n\n scheduleRenewal();\n }\n };\n\n // Internal function to schedule renewal based on the expiration date in\n // the existing token. Renewal happens 15 minutes before expiration.\n const scheduleRenewal = (): void => {\n // Cancel any existing renewal timer.\n if (renewalTimeout !== null) {\n clearTimeout(renewalTimeout);\n renewalTimeout = null;\n }\n\n // No token, nothing to do.\n if (tokenRef.value === null) {\n return;\n }\n\n const segments = tokenRef.value?.split(\";\");\n\n // Token not in expected format.\n if (segments.length !== 3 || segments[0] !== \"1\") {\n return;\n }\n\n const expiresDateTime = RockDateTime.parseISO(segments[1]);\n\n // Could not parse expiration date and time.\n if (expiresDateTime === null) {\n return;\n }\n\n const renewTimeout = expiresDateTime.addMinutes(-15).toMilliseconds() - RockDateTime.now().toMilliseconds();\n\n // Renewal request would be in the past, ignore.\n if (renewTimeout < 0) {\n return;\n }\n\n // Schedule the renewal task to happen 15 minutes before expiration.\n renewalTimeout = setTimeout(renewToken, renewTimeout);\n };\n\n scheduleRenewal();\n\n return {\n token: tokenRef,\n updateToken(newToken) {\n tokenRef.value = newToken || null;\n scheduleRenewal();\n }\n };\n}\n\n/**\n * Provides the security grant to child components to use in their API calls.\n *\n * @param grant The grant ot provide to child components.\n */\nexport function provideSecurityGrant(grant: SecurityGrant): void {\n provide(securityGrantSymbol, grant);\n}\n\n/**\n * Uses a previously provided security grant token by a parent component.\n * This function is meant to be used by controls that need to obtain a security\n * grant from a parent component.\n *\n * @returns A string reference that contains the security grant token.\n */\nexport function useSecurityGrantToken(): Ref {\n const grant = inject(securityGrantSymbol);\n\n return grant ? grant.token : ref(null);\n}\n\n// #endregion\n\n// #region Extended References\n\n/** An emit object that conforms to having a propertyChanged event. */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport type PropertyChangedEmitFn = E extends Array ? (event: EE, ...args: any[]) => void : (event: E, ...args: any[]) => void;\n\n/**\n * Watches for changes to the given Ref objects and emits a special event to\n * indicate that a given property has changed.\n *\n * @param propertyRefs The ExtendedRef objects to watch for changes.\n * @param emit The emit function for the component.\n */\nexport function watchPropertyChanges(propertyRefs: ExtendedRef[], emit: PropertyChangedEmitFn): void {\n for (const propRef of propertyRefs) {\n watch(propRef, () => {\n if (propRef.context.propertyName) {\n emit(\"propertyChanged\", propRef.context.propertyName);\n }\n });\n }\n}\n\n/**\n * Requests an updated attribute list from the server based on the\n * current UI selections made.\n *\n * @param box The valid properties box that will be used to determine current\n * property values and then updated with the new attributes and values.\n * @param invokeBlockAction The function to use when calling the block action.\n */\nasync function refreshEntityDetailAttributes(box: Ref>, invokeBlockAction: InvokeBlockActionFunc): Promise {\n const result = await invokeBlockAction>(\"RefreshAttributes\", {\n box: box.value\n });\n\n if (result.isSuccess) {\n if (result.statusCode === 200 && result.data && box.value) {\n const newBox: ValidPropertiesBox = {\n ...box.value,\n bag: {\n ...box.value.bag as TEntityBag,\n attributes: result.data.bag?.attributes,\n attributeValues: result.data.bag?.attributeValues\n }\n };\n\n box.value = newBox;\n }\n }\n}\n\n/**\n * Requests an updated attribute list from the server based on the\n * current UI selections made.\n *\n * @param bag The entity bag that will be used to determine current property values\n * and then updated with the new attributes and values.\n * @param validProperties The properties that are considered valid on the bag when\n * the server will read the bag.\n * @param invokeBlockAction The function to use when calling the block action.\n */\nexport async function refreshDetailAttributes(bag: Ref, validProperties: string[], invokeBlockAction: InvokeBlockActionFunc): Promise {\n const data: DetailBlockBox = {\n entity: bag.value,\n isEditable: true,\n validProperties: validProperties\n };\n\n const result = await invokeBlockAction, unknown>>(\"RefreshAttributes\", {\n box: data\n });\n\n if (result.isSuccess) {\n if (result.statusCode === 200 && result.data && bag.value) {\n const newBag: TEntityBag = {\n ...bag.value,\n attributes: result.data.entity?.attributes,\n attributeValues: result.data.entity?.attributeValues\n };\n\n bag.value = newBag;\n }\n }\n}\n\n// #endregion Extended Refs\n\n// #region Block and BlockType Guid\n\nconst blockGuidSymbol = Symbol(\"block-guid\");\nconst blockTypeGuidSymbol = Symbol(\"block-type-guid\");\n\n/**\n * Provides the block unique identifier to all child components.\n * This is an internal method and should not be used by plugins.\n *\n * @param blockGuid The unique identifier of the block.\n */\nexport function provideBlockGuid(blockGuid: string): void {\n provide(blockGuidSymbol, blockGuid);\n}\n\n/**\n * Gets the unique identifier of the current block in this component chain.\n *\n * @returns The unique identifier of the block.\n */\nexport function useBlockGuid(): Guid | undefined {\n return inject(blockGuidSymbol);\n}\n\n/**\n * Provides the block type unique identifier to all child components.\n * This is an internal method and should not be used by plugins.\n *\n * @param blockTypeGuid The unique identifier of the block type.\n */\nexport function provideBlockTypeGuid(blockTypeGuid: string): void {\n provide(blockTypeGuidSymbol, blockTypeGuid);\n}\n\n/**\n * Gets the block type unique identifier of the current block in this component\n * chain.\n *\n * @returns The unique identifier of the block type.\n */\nexport function useBlockTypeGuid(): Guid | undefined {\n return inject(blockTypeGuidSymbol);\n}\n\n// #endregion\n\n// #region Person Preferences\n\nconst blockPreferenceProviderSymbol = Symbol();\n\n/** An no-op implementation of {@link IPersonPreferenceCollection}. */\nconst emptyPreferences: IPersonPreferenceCollection = {\n getValue(): string {\n return \"\";\n },\n setValue(): void {\n // Intentionally empty.\n },\n getKeys(): string[] {\n return [];\n },\n containsKey(): boolean {\n return false;\n },\n save(): Promise {\n return Promise.resolve();\n },\n withPrefix(): IPersonPreferenceCollection {\n return emptyPreferences;\n },\n on(): void {\n // Intentionally empty.\n },\n off(): void {\n // Intentionally empty.\n }\n};\n\nconst emptyPreferenceProvider: IBlockPersonPreferencesProvider = {\n blockPreferences: emptyPreferences,\n getGlobalPreferences() {\n return Promise.resolve(emptyPreferences);\n },\n getEntityPreferences() {\n return Promise.resolve(emptyPreferences);\n }\n};\n\n/**\n * Provides the person preferences provider that will be used by components\n * to access the person preferences associated with their block.\n *\n * @private This is an internal method and should not be used by plugins.\n *\n * @param blockGuid The unique identifier of the block.\n */\nexport function providePersonPreferences(provider: IBlockPersonPreferencesProvider): void {\n provide(blockPreferenceProviderSymbol, provider);\n}\n\n/**\n * Gets the person preference provider that can be used to access block\n * preferences as well as other preferences.\n *\n * @returns An object that implements {@link IBlockPersonPreferencesProvider}.\n */\nexport function usePersonPreferences(): IBlockPersonPreferencesProvider {\n return inject(blockPreferenceProviderSymbol)\n ?? emptyPreferenceProvider;\n}\n\n// #endregion\n","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n//\n\n/**\n * Transform the value into true, false, or null\n * @param val\n */\nexport function asBooleanOrNull(val: unknown): boolean | null {\n if (val === undefined || val === null) {\n return null;\n }\n\n if (typeof val === \"boolean\") {\n return val;\n }\n\n if (typeof val === \"string\") {\n const asString = (val || \"\").trim().toLowerCase();\n\n if (!asString) {\n return null;\n }\n\n return [\"true\", \"yes\", \"t\", \"y\", \"1\"].indexOf(asString) !== -1;\n }\n\n if (typeof val === \"number\") {\n return !!val;\n }\n\n return null;\n}\n\n/**\n * Transform the value into true or false\n * @param val\n */\nexport function asBoolean(val: unknown): boolean {\n return !!asBooleanOrNull(val);\n}\n\n/** Transform the value into the strings \"Yes\", \"No\", or null */\nexport function asYesNoOrNull(val: unknown): \"Yes\" | \"No\" | null {\n const boolOrNull = asBooleanOrNull(val);\n\n if (boolOrNull === null) {\n return null;\n }\n\n return boolOrNull ? \"Yes\" : \"No\";\n}\n\n/** Transform the value into the strings \"True\", \"False\", or null */\nexport function asTrueFalseOrNull(val: unknown): \"True\" | \"False\" | null {\n const boolOrNull = asBooleanOrNull(val);\n\n if (boolOrNull === null) {\n return null;\n }\n\n return boolOrNull ? \"True\" : \"False\";\n}\n\n/** Transform the value into the strings \"True\" if truthy or \"False\" if falsey */\nexport function asTrueOrFalseString(val: unknown): \"True\" | \"False\" {\n const boolOrNull = asBooleanOrNull(val);\n\n return boolOrNull ? \"True\" : \"False\";\n}\n","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n\nimport { RockDateTime } from \"./rockDateTime\";\n\n//\ntype CacheEntry = {\n value: T;\n expiration: number;\n};\n\n/**\n* Stores the value using the given key. The cache will expire at the expiration or in\n* 1 minute if none is provided\n* @param key\n* @param value\n* @param expiration\n*/\nfunction set(key: string, value: T, expirationDT: RockDateTime | null = null): void {\n let expiration: number;\n\n if (expirationDT) {\n expiration = expirationDT.toMilliseconds();\n }\n else {\n // Default to one minute\n expiration = RockDateTime.now().addMinutes(1).toMilliseconds();\n }\n\n const cache: CacheEntry = { expiration, value };\n const cacheJson = JSON.stringify(cache);\n sessionStorage.setItem(key, cacheJson);\n}\n\n/**\n * Gets a stored cache value if there is one that has not yet expired.\n * @param key\n */\nfunction get(key: string): T | null {\n const cacheJson = sessionStorage.getItem(key);\n\n if (!cacheJson) {\n return null;\n }\n\n const cache = JSON.parse(cacheJson) as CacheEntry;\n\n if (!cache || !cache.expiration) {\n return null;\n }\n\n if (cache.expiration < RockDateTime.now().toMilliseconds()) {\n return null;\n }\n\n return cache.value;\n}\n\nconst promiseCache: Record | undefined> = {};\n\n/**\n * Since Promises can't be cached, we need to store them in memory until we get the result back. This wraps\n * a function in another function that returns a promise and...\n * - If there's a cached result, return it\n * - Otherwise if there's a cached Promise, return it\n * - Otherwise call the given function and cache it's promise and return it. Once the the Promise resolves, cache its result\n *\n * @param key Key for identifying the cached values\n * @param fn Function that returns a Promise that we want to cache the value of\n *\n */\nfunction cachePromiseFactory(key: string, fn: () => Promise, expiration: RockDateTime | null = null): () => Promise {\n return async function (): Promise {\n // If it's cached, grab it\n const cachedResult = get(key);\n if (cachedResult) {\n return cachedResult;\n }\n\n // If it's not cached yet but we've already started fetching it\n // (it's not cached until we receive the results), return the existing Promise\n if (promiseCache[key]) {\n return promiseCache[key] as Promise;\n }\n\n // Not stored anywhere, so fetch it and save it on the stored Promise for the next call\n promiseCache[key] = fn();\n\n // Once it's resolved, cache the result\n promiseCache[key]?.then((result) => {\n set(key, result, expiration);\n delete promiseCache[key];\n return result;\n }).catch((e: Error) => {\n // Something's wrong, let's get rid of the stored promise, so we can try again.\n delete promiseCache[key];\n throw e;\n });\n\n return promiseCache[key] as Promise;\n };\n}\n\n\nexport default {\n set,\n get,\n cachePromiseFactory: cachePromiseFactory\n};\n","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n//\n\nimport { Guid } from \"@Obsidian/Types\";\nimport { newGuid } from \"./guid\";\nimport { inject, nextTick, provide } from \"vue\";\n\nconst suspenseSymbol = Symbol(\"RockSuspense\");\n\n/**\n * Defines the interface for a provider of suspense monitoring. These are used\n * to track asynchronous operations that components may be performing so the\n * watching component can perform an operation once all pending operations\n * have completed.\n */\nexport interface ISuspenseProvider {\n /**\n * Adds a new operation identified by the promise. When the promise\n * either resolves or fails the operation is considered completed.\n *\n * @param operation The promise that represents the operation.\n */\n addOperation(operation: Promise): void;\n\n /**\n * Notes that an asynchronous operation has started on a child component.\n *\n * @param key The key that identifies the operation.\n */\n startAsyncOperation(key: Guid): void;\n\n /**\n * Notes that an asynchrounous operation has completed on a child component.\n *\n * @param key The key that was previously passed to startAsyncOperation.\n */\n completeAsyncOperation(key: Guid): void;\n}\n\n/**\n * A basic provider that handles the guts of a suspense provider. This can be\n * used by components that need to know when child components have completed\n * their work.\n */\nexport class BasicSuspenseProvider implements ISuspenseProvider {\n private readonly operationKey: Guid;\n\n private readonly parentProvider: ISuspenseProvider | undefined;\n\n private readonly pendingOperations: Guid[];\n\n private finishedHandlers: (() => void)[];\n\n /**\n * Creates a new suspense provider.\n *\n * @param parentProvider The parent suspense provider that will be notified of pending operations.\n */\n constructor(parentProvider: ISuspenseProvider | undefined) {\n this.operationKey = newGuid();\n this.parentProvider = parentProvider;\n this.pendingOperations = [];\n this.finishedHandlers = [];\n }\n\n /**\n * Called when all pending operations are complete. Notifies all handlers\n * that the pending operations have completed as well as the parent provider.\n */\n private allOperationsComplete(): void {\n // Wait until the next Vue tick in case a new async operation started.\n // This can happen, for example, with defineAsyncComponent(). It will\n // complete its async operation (loading the JS file) and then the\n // component defined in the file might start an async operation. This\n // prevents us from completing too soon.\n nextTick(() => {\n // Verify nothing started a new asynchronous operation while we\n // we waiting for the next tick.\n if (this.pendingOperations.length !== 0) {\n return;\n }\n\n // Notify all pending handlers that all operations completed.\n for (const handler of this.finishedHandlers) {\n handler();\n }\n this.finishedHandlers = [];\n\n // Notify the parent that our own pending operation has completed.\n if (this.parentProvider) {\n this.parentProvider.completeAsyncOperation(this.operationKey);\n }\n });\n }\n\n /**\n * Adds a new operation identified by the promise. When the promise\n * either resolves or fails the operation is considered completed.\n *\n * @param operation The promise that represents the operation.\n */\n public addOperation(operation: Promise): void {\n const operationKey = newGuid();\n\n this.startAsyncOperation(operationKey);\n\n operation.then(() => this.completeAsyncOperation(operationKey))\n .catch(() => this.completeAsyncOperation(operationKey));\n }\n\n /**\n * Notes that an asynchronous operation has started on a child component.\n *\n * @param key The key that identifies the operation.\n */\n public startAsyncOperation(key: Guid): void {\n this.pendingOperations.push(key);\n\n // If this is the first operation we started, notify the parent provider.\n if (this.pendingOperations.length === 1 && this.parentProvider) {\n this.parentProvider.startAsyncOperation(this.operationKey);\n }\n }\n\n /**\n * Notes that an asynchrounous operation has completed on a child component.\n *\n * @param key The key that was previously passed to startAsyncOperation.\n */\n public completeAsyncOperation(key: Guid): void {\n const index = this.pendingOperations.indexOf(key);\n\n if (index !== -1) {\n this.pendingOperations.splice(index, 1);\n }\n\n // If this was the last operation then send notifications.\n if (this.pendingOperations.length === 0) {\n this.allOperationsComplete();\n }\n }\n\n /**\n * Checks if this provider has any asynchronous operations that are still\n * pending completion.\n *\n * @returns true if there are pending operations; otherwise false.\n */\n public hasPendingOperations(): boolean {\n return this.pendingOperations.length > 0;\n }\n\n /**\n * Adds a new handler that is called when all pending operations have been\n * completed. This is a fire-once, meaning the callback will only be called\n * when the current pending operations have completed. If new operations\n * begin after the callback is executed it will not be called again unless\n * it is added with this method again.\n *\n * @param callback The function to call when all pending operations have completed.\n */\n public addFinishedHandler(callback: () => void): void {\n this.finishedHandlers.push(callback);\n }\n}\n\n/**\n * Provides a new suspense provider to any child components.\n *\n * @param provider The provider to make available to child components.\n */\nexport function provideSuspense(provider: ISuspenseProvider): void {\n provide(suspenseSymbol, provider);\n}\n\n/**\n * Uses the current suspense provider that was defined by any parent component.\n *\n * @returns The suspense provider if one was defined; otherwise undefined.\n */\nexport function useSuspense(): ISuspenseProvider | undefined {\n return inject(suspenseSymbol);\n}\n\n","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n\nimport { CurrencyInfoBag } from \"../ViewModels/Rest/Utilities/currencyInfoBag\";\n\n// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat\n// Number.toLocaleString takes the same options as Intl.NumberFormat\n// Most of the options probably won't get used, so just add the ones you need to use to this when needed\ntype NumberFormatOptions = {\n useGrouping?: boolean // MDN gives other possible values, but TS is complaining that it should only be boolean\n};\n\n/**\n * Get a formatted string.\n * Ex: 10001.2 => 10,001.2\n * @param num\n */\nexport function asFormattedString(num: number | null, digits?: number, options: NumberFormatOptions = {}): string {\n if (num === null) {\n return \"\";\n }\n\n return num.toLocaleString(\n \"en-US\",\n {\n minimumFractionDigits: digits,\n maximumFractionDigits: digits ?? 9,\n ...options\n }\n );\n}\n\n/**\n * Get a number value from a formatted string. If the number cannot be parsed, then zero is returned by default.\n * Ex: $1,000.20 => 1000.2\n * @param str\n */\nexport function toNumber(str?: string | number | null): number {\n return toNumberOrNull(str) || 0;\n}\n\n/**\n * Get a number value from a formatted string. If the number cannot be parsed, then null is returned by default.\n * Ex: $1,000.20 => 1000.2\n * @param str\n */\nexport function toNumberOrNull(str?: string | number | null): number | null {\n if (str === null || str === undefined || str === \"\") {\n return null;\n }\n\n if (typeof str === \"number\") {\n return str;\n }\n\n const replaced = str.replace(/[$,]/g, \"\");\n const num = Number(replaced);\n\n return !isNaN(num) ? num : null;\n}\n\n/**\n * Get a currency value from a string or number. If the number cannot be parsed, then null is returned by default.\n * Ex: 1000.20 => $1,000.20\n * @param value The value to be converted to a currency.\n */\nexport function toCurrencyOrNull(value?: string | number | null, currencyInfo: CurrencyInfoBag | null = null): string | null {\n if (typeof value === \"string\") {\n value = toNumberOrNull(value);\n }\n\n if (value === null || value === undefined) {\n return null;\n }\n const currencySymbol = currencyInfo?.symbol ?? \"$\";\n const currencyDecimalPlaces = currencyInfo?.decimalPlaces ?? 2;\n return `${currencySymbol}${asFormattedString(value, currencyDecimalPlaces)}`;\n}\n\n/**\n * Adds an ordinal suffix.\n * Ex: 1 => 1st\n * @param num\n */\nexport function toOrdinalSuffix(num?: number | null): string {\n if (!num) {\n return \"\";\n }\n\n const j = num % 10;\n const k = num % 100;\n\n if (j == 1 && k != 11) {\n return num + \"st\";\n }\n if (j == 2 && k != 12) {\n return num + \"nd\";\n }\n if (j == 3 && k != 13) {\n return num + \"rd\";\n }\n return num + \"th\";\n}\n\n/**\n * Convert a number to an ordinal.\n * Ex: 1 => first, 10 => tenth\n *\n * Anything larger than 10 will be converted to the number with an ordinal suffix.\n * Ex: 123 => 123rd, 1000 => 1000th\n * @param num\n */\nexport function toOrdinal(num?: number | null): string {\n if (!num) {\n return \"\";\n }\n\n switch (num) {\n case 1: return \"first\";\n case 2: return \"second\";\n case 3: return \"third\";\n case 4: return \"fourth\";\n case 5: return \"fifth\";\n case 6: return \"sixth\";\n case 7: return \"seventh\";\n case 8: return \"eighth\";\n case 9: return \"ninth\";\n case 10: return \"tenth\";\n default: return toOrdinalSuffix(num);\n }\n}\n\n/**\n * Convert a number to a word.\n * Ex: 1 => \"one\", 10 => \"ten\"\n *\n * Anything larger than 10 will be returned as a number string instead of a word.\n * Ex: 123 => \"123\", 1000 => \"1000\"\n * @param num\n */\nexport function toWord(num?: number | null): string {\n if (num === null || num === undefined) {\n return \"\";\n }\n\n switch (num) {\n case 1: return \"one\";\n case 2: return \"two\";\n case 3: return \"three\";\n case 4: return \"four\";\n case 5: return \"five\";\n case 6: return \"six\";\n case 7: return \"seven\";\n case 8: return \"eight\";\n case 9: return \"nine\";\n case 10: return \"ten\";\n default: return `${num}`;\n }\n}\n\nexport function zeroPad(num: number, length: number): string {\n let str = num.toString();\n\n while (str.length < length) {\n str = \"0\" + str;\n }\n\n return str;\n}\n\nexport function toDecimalPlaces(num: number, decimalPlaces: number): number {\n decimalPlaces = Math.floor(decimalPlaces); // ensure it's an integer\n\n return Math.round(num * 10 ** decimalPlaces) / 10 ** decimalPlaces;\n}\n\n/**\n * Returns the string representation of an integer.\n * Ex: 1 => \"1\", 123456 => \"one hundred twenty-three thousand four hundred fifty-six\"\n *\n * Not reliable for numbers in the quadrillions and greater.\n *\n * @example\n * numberToWord(1) // one\n * numberToWord(2) // two\n * numberToWord(123456) // one hundred twenty-three thousand four hundred fifty-six\n * @param numb The number for which to get the string representation.\n * @returns \"one\", \"two\", ..., \"one thousand\", ..., (up to the max number allowed for JS).\n */\nexport function toWordFull(numb: number): string {\n const numberWords = {\n 0: \"zero\",\n 1: \"one\",\n 2: \"two\",\n 3: \"three\",\n 4: \"four\",\n 5: \"five\",\n 6: \"six\",\n 7: \"seven\",\n 8: \"eight\",\n 9: \"nine\",\n 10: \"ten\",\n 11: \"eleven\",\n 12: \"twelve\",\n 13: \"thirteen\",\n 14: \"fourteen\",\n 15: \"fifteen\",\n 16: \"sixteen\",\n 17: \"seventeen\",\n 18: \"eighteen\",\n 19: \"nineteen\",\n 20: \"twenty\",\n 30: \"thirty\",\n 40: \"forty\",\n 50: \"fifty\",\n 60: \"sixty\",\n 70: \"seventy\",\n 80: \"eighty\",\n 90: \"ninety\",\n 100: \"one hundred\",\n 1000: \"one thousand\",\n 1000000: \"one million\",\n 1000000000: \"one billion\",\n 1000000000000: \"one trillion\",\n 1000000000000000: \"one quadrillion\"\n };\n\n // Store constants for these since it is hard to distinguish between them at larger numbers.\n const oneHundred = 100;\n const oneThousand = 1000;\n const oneMillion = 1000000;\n const oneBillion = 1000000000;\n const oneTrillion = 1000000000000;\n const oneQuadrillion = 1000000000000000;\n\n if (numberWords[numb]) {\n return numberWords[numb];\n }\n\n function quadrillionsToWord(numb: number): string {\n const trillions = trillionsToWord(numb);\n if (numb >= oneQuadrillion) {\n const quadrillions = hundredsToWord(Number(numb.toString().slice(-18, -15)));\n if (trillions) {\n return `${quadrillions} quadrillion ${trillions}`;\n }\n else {\n return `${quadrillions} quadrillion`;\n }\n }\n else {\n return trillions;\n }\n }\n\n function trillionsToWord(numb: number): string {\n numb = Number(numb.toString().slice(-15));\n const billions = billionsToWord(numb);\n if (numb >= oneTrillion) {\n const trillions = hundredsToWord(Number(numb.toString().slice(-15, -12)));\n if (billions) {\n return `${trillions} trillion ${billions}`;\n }\n else {\n return `${trillions} trillion`;\n }\n }\n else {\n return billions;\n }\n }\n\n function billionsToWord(numb: number): string {\n numb = Number(numb.toString().slice(-12));\n const millions = millionsToWord(numb);\n if (numb >= oneBillion) {\n const billions = hundredsToWord(Number(numb.toString().slice(-12, -9)));\n if (millions) {\n return `${billions} billion ${millions}`;\n }\n else {\n return `${billions} billion`;\n }\n }\n else {\n return millions;\n }\n }\n\n function millionsToWord(numb: number): string {\n numb = Number(numb.toString().slice(-9));\n const thousands = thousandsToWord(numb);\n if (numb >= oneMillion) {\n const millions = hundredsToWord(Number(numb.toString().slice(-9, -6)));\n if (thousands) {\n return `${millions} million ${thousands}`;\n }\n else {\n return `${millions} million`;\n }\n }\n else {\n return thousands;\n }\n }\n\n function thousandsToWord(numb: number): string {\n numb = Number(numb.toString().slice(-6));\n const hundreds = hundredsToWord(numb);\n if (numb >= oneThousand) {\n const thousands = hundredsToWord(Number(numb.toString().slice(-6, -3)));\n if (hundreds) {\n return `${thousands} thousand ${hundreds}`;\n }\n else {\n return `${thousands} thousandths`;\n }\n }\n else {\n return hundreds;\n }\n }\n\n function hundredsToWord(numb: number): string {\n numb = Number(numb.toString().slice(-3));\n\n if (numberWords[numb]) {\n return numberWords[numb];\n }\n\n const tens = tensToWord(numb);\n\n if (numb >= oneHundred) {\n const hundreds = Number(numb.toString().slice(-3, -2));\n if (tens) {\n return `${numberWords[hundreds]} hundred ${tens}`;\n }\n else {\n return `${numberWords[hundreds]} hundred`;\n }\n }\n else {\n return tens;\n }\n }\n\n function tensToWord(numb: number): string {\n numb = Number(numb.toString().slice(-2));\n\n if (numberWords[numb]) {\n return numberWords[numb];\n }\n\n const ones = onesToWord(numb);\n\n if (numb >= 20) {\n const tens = Number(numb.toString().slice(-2, -1));\n\n if (ones) {\n return `${numberWords[tens * 10]}-${ones}`;\n }\n else {\n return numberWords[tens * 10];\n }\n }\n else {\n return ones;\n }\n }\n\n function onesToWord(numb: number): string {\n numb = Number(numb.toString().slice(-1));\n return numberWords[numb];\n }\n\n return quadrillionsToWord(numb);\n}\n\nexport default {\n toOrdinal,\n toOrdinalSuffix,\n toNumberOrNull,\n asFormattedString\n};\n","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n\nimport { AsyncComponentLoader, Component, ComponentPublicInstance, defineAsyncComponent as vueDefineAsyncComponent, ExtractPropTypes, PropType, reactive, ref, Ref, VNode, watch, WatchOptions, render, isVNode, createVNode } from \"vue\";\nimport { deepEqual } from \"./util\";\nimport { useSuspense } from \"./suspense\";\nimport { newGuid } from \"./guid\";\nimport { ControlLazyMode } from \"@Obsidian/Enums/Controls/controlLazyMode\";\nimport { PickerDisplayStyle } from \"@Obsidian/Enums/Controls/pickerDisplayStyle\";\nimport { ExtendedRef, ExtendedRefContext } from \"@Obsidian/Types/Utility/component\";\nimport type { RulesPropType, ValidationRule } from \"@Obsidian/Types/validationRules\";\nimport { toNumberOrNull } from \"./numberUtils\";\n\ntype Prop = { [key: string]: unknown };\ntype PropKey = Extract;\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype EmitFn = E extends Array ? (event: EE, ...args: any[]) => void : (event: E, ...args: any[]) => void;\n\n/**\n * Utility function for when you are using a component that takes a v-model\n * and uses that model as a v-model in that component's template. It creates\n * a new ref that keeps itself up-to-date with the given model and fires an\n * 'update:MODELNAME' event when it gets changed.\n *\n * Ensure the related `props` and `emits` are specified to ensure there are\n * no type issues.\n */\nexport function useVModelPassthrough, E extends `update:${K}`>(props: T, modelName: K, emit: EmitFn, options?: WatchOptions): Ref {\n const internalValue = ref(props[modelName]) as Ref;\n\n watch(() => props[modelName], val => updateRefValue(internalValue, val), options);\n watch(internalValue, val => {\n if (val !== props[modelName]) {\n emit(`update:${modelName}`, val);\n }\n }, options);\n\n return internalValue;\n}\n\n/**\n * Utility function for when you are using a component that takes a v-model\n * and uses that model as a v-model in that component's template. It creates\n * a new ref that keeps itself up-to-date with the given model and fires an\n * 'update:MODELNAME' event when it gets changed. It also gives a means of watching\n * the model prop for any changes (verifies that the prop change is different than\n * the current value first)\n *\n * Ensure the related `props` and `emits` are specified to ensure there are\n * no type issues.\n */\nexport function useVModelPassthroughWithPropUpdateCheck, E extends `update:${K}`>(props: T, modelName: K, emit: EmitFn, options?: WatchOptions): [Ref, (fn: () => unknown) => void] {\n const internalValue = ref(props[modelName]) as Ref;\n const listeners: (() => void)[] = [];\n\n watch(() => props[modelName], val => {\n if (updateRefValue(internalValue, val)) {\n onPropUpdate();\n }\n }, options);\n watch(internalValue, val => emit(`update:${modelName}`, val), options);\n\n function onPropUpdate(): void {\n listeners.forEach(fn => fn());\n }\n\n function addPropUpdateListener(fn: () => unknown): void {\n listeners.push(fn);\n }\n\n return [internalValue, addPropUpdateListener];\n}\n\n/**\n * Updates the Ref value, but only if the new value is actually different than\n * the current value. A deep comparison is performed.\n *\n * @param target The target Ref object to be updated.\n * @param value The new value to be assigned to the target.\n *\n * @returns True if the target was updated, otherwise false.\n */\nexport function updateRefValue(target: Ref, value: TV): boolean {\n if (deepEqual(target.value, value, true)) {\n return false;\n }\n\n target.value = value;\n\n return true;\n}\n\n/**\n * Defines a component that will be loaded asynchronously. This contains logic\n * to properly work with the RockSuspense control.\n *\n * @param source The function to call to load the component.\n *\n * @returns The component that was loaded.\n */\nexport function defineAsyncComponent(source: AsyncComponentLoader): T {\n return vueDefineAsyncComponent(async () => {\n const suspense = useSuspense();\n const operationKey = newGuid();\n\n suspense?.startAsyncOperation(operationKey);\n const component = await source();\n suspense?.completeAsyncOperation(operationKey);\n\n return component;\n });\n}\n\n// #region Standard Form Field\n\ntype StandardRockFormFieldProps = {\n label: {\n type: PropType,\n default: \"\"\n },\n\n help: {\n type: PropType,\n default: \"\"\n },\n\n rules: RulesPropType,\n\n formGroupClasses: {\n type: PropType,\n default: \"\"\n },\n\n validationTitle: {\n type: PropType,\n default: \"\"\n },\n\n isRequiredIndicatorHidden: {\n type: PropType,\n default: false\n }\n};\n\n/** The standard component props that should be included when using RockFormField. */\nexport const standardRockFormFieldProps: StandardRockFormFieldProps = {\n label: {\n type: String as PropType,\n default: \"\"\n },\n\n help: {\n type: String as PropType,\n default: \"\"\n },\n\n rules: {\n type: [Array, Object, String] as PropType,\n default: \"\"\n },\n\n formGroupClasses: {\n type: String as PropType,\n default: \"\"\n },\n\n validationTitle: {\n type: String as PropType,\n default: \"\"\n },\n\n isRequiredIndicatorHidden: {\n type: Boolean as PropType,\n default: false\n }\n};\n\n/**\n * Copies the known properties for the standard rock form field props from\n * the source object to the destination object.\n *\n * @param source The source object to copy the values from.\n * @param destination The destination object to copy the values to.\n */\nfunction copyStandardRockFormFieldProps(source: ExtractPropTypes, destination: ExtractPropTypes): void {\n destination.formGroupClasses = source.formGroupClasses;\n destination.help = source.help;\n destination.label = source.label;\n destination.rules = source.rules;\n destination.validationTitle = source.validationTitle;\n}\n\n/**\n * Configures the basic properties that should be passed to the RockFormField\n * component. The value returned by this function should be used with v-bind on\n * the RockFormField in order to pass all the defined prop values to it.\n *\n * @param props The props of the component that will be using the RockFormField.\n *\n * @returns An object of prop values that can be used with v-bind.\n */\nexport function useStandardRockFormFieldProps(props: ExtractPropTypes): ExtractPropTypes {\n const propValues = reactive>({\n label: props.label,\n help: props.help,\n rules: props.rules,\n formGroupClasses: props.formGroupClasses,\n validationTitle: props.validationTitle,\n isRequiredIndicatorHidden: props.isRequiredIndicatorHidden\n });\n\n watch([() => props.formGroupClasses, () => props.help, () => props.label, () => props.rules, () => props.validationTitle], () => {\n copyStandardRockFormFieldProps(props, propValues);\n });\n\n return propValues;\n}\n\n// #endregion\n\n// #region Standard Async Pickers\n\ntype StandardAsyncPickerProps = StandardRockFormFieldProps & {\n /** Enhance the picker for dealing with long lists by providing a search mechanism. */\n enhanceForLongLists: {\n type: PropType,\n default: false\n },\n\n /** The method the picker should use to load data. */\n lazyMode: {\n type: PropType,\n default: \"onDemand\"\n },\n\n /** True if the picker should allow multiple items to be selected. */\n multiple: {\n type: PropType,\n default: false\n },\n\n /** True if the picker should allow empty selections. */\n showBlankItem: {\n type: PropType,\n default: false\n },\n\n /** The optional value to show when `showBlankItem` is `true`. */\n blankValue: {\n type: PropType,\n default: \"\"\n },\n\n /** The visual style to use when displaying the picker. */\n displayStyle: {\n type: PropType,\n default: \"auto\"\n },\n\n /** The number of columns to use when displaying the items in a list. */\n columnCount: {\n type: PropType,\n default: 0\n }\n};\n\n/** The standard component props that should be included when using BaseAsyncPicker. */\nexport const standardAsyncPickerProps: StandardAsyncPickerProps = {\n ...standardRockFormFieldProps,\n\n enhanceForLongLists: {\n type: Boolean as PropType,\n default: false\n },\n\n lazyMode: {\n type: String as PropType,\n default: ControlLazyMode.OnDemand\n },\n\n multiple: {\n type: Boolean as PropType,\n default: false\n },\n\n showBlankItem: {\n type: Boolean as PropType,\n default: false\n },\n\n blankValue: {\n type: String as PropType,\n default: \"\"\n },\n\n displayStyle: {\n type: String as PropType,\n default: PickerDisplayStyle.Auto\n },\n\n columnCount: {\n type: Number as PropType,\n default: 0\n }\n};\n\n/**\n * Copies the known properties for the standard async picker props from\n * the source object to the destination object.\n *\n * @param source The source object to copy the values from.\n * @param destination The destination object to copy the values to.\n */\nfunction copyStandardAsyncPickerProps(source: ExtractPropTypes, destination: ExtractPropTypes): void {\n copyStandardRockFormFieldProps(source, destination);\n\n destination.enhanceForLongLists = source.enhanceForLongLists;\n destination.lazyMode = source.lazyMode;\n destination.multiple = source.multiple;\n destination.showBlankItem = source.showBlankItem;\n destination.blankValue = source.blankValue;\n destination.displayStyle = source.displayStyle;\n destination.columnCount = source.columnCount;\n}\n\n/**\n * Configures the basic properties that should be passed to the BaseAsyncPicker\n * component. The value returned by this function should be used with v-bind on\n * the BaseAsyncPicker in order to pass all the defined prop values to it.\n *\n * @param props The props of the component that will be using the BaseAsyncPicker.\n *\n * @returns An object of prop values that can be used with v-bind.\n */\nexport function useStandardAsyncPickerProps(props: ExtractPropTypes): ExtractPropTypes {\n const standardFieldProps = useStandardRockFormFieldProps(props);\n\n const propValues = reactive>({\n ...standardFieldProps,\n enhanceForLongLists: props.enhanceForLongLists,\n lazyMode: props.lazyMode,\n multiple: props.multiple,\n showBlankItem: props.showBlankItem,\n blankValue: props.blankValue,\n displayStyle: props.displayStyle,\n columnCount: props.columnCount\n });\n\n // Watch for changes in any of the standard props. Use deep for this so we\n // don't need to know which prop keys it actually contains.\n watch(() => standardFieldProps, () => {\n copyStandardRockFormFieldProps(props, propValues);\n }, {\n deep: true\n });\n\n // Watch for changes in our known list of props that might change.\n watch([() => props.enhanceForLongLists, () => props.lazyMode, () => props.multiple, () => props.showBlankItem, () => props.displayStyle, () => props.columnCount], () => {\n copyStandardAsyncPickerProps(props, propValues);\n });\n\n return propValues;\n}\n\n// #endregion\n\n// #region Extended References\n\n/**\n * Creates a Ref that contains extended data to better identify this ref\n * when you have multiple refs to work with.\n *\n * @param value The initial value of the Ref.\n * @param extendedData The additional context data to put on the Ref.\n *\n * @returns An ExtendedRef object that can be used like a regular Ref object.\n */\nexport function extendedRef(value: T, context: ExtendedRefContext): ExtendedRef {\n const refValue = ref(value) as ExtendedRef;\n\n refValue.context = context;\n\n return refValue;\n}\n\n/**\n * Creates an extended Ref with the specified property name in the context.\n *\n * @param value The initial value of the Ref.\n * @param propertyName The property name to use for the context.\n *\n * @returns An ExtendedRef object that can be used like a regular Ref object.\n */\nexport function propertyRef(value: T, propertyName: string): ExtendedRef {\n return extendedRef(value, {\n propertyName\n });\n}\n\n// #endregion Extended Refs\n\n// #region VNode Helpers\n\n/**\n * Retrieves a single prop value from a VNode object. If the prop is explicitely\n * specified in the DOM then it will be returned. Otherwise the component's\n * prop default values are checked. If there is a default value it will be\n * returned.\n *\n * @param node The node whose property value is being requested.\n * @param propName The name of the property whose value is being requested.\n *\n * @returns The value of the property or `undefined` if it was not set.\n */\nexport function getVNodeProp(node: VNode, propName: string): T | undefined {\n // Check if the prop was specified in the DOM declaration.\n if (node.props && node.props[propName] !== undefined) {\n return node.props[propName] as T;\n }\n\n // Now look to see if the backing component has defined a prop with that\n // name and provided a default value.\n if (typeof node.type === \"object\" && typeof node.type[\"props\"] === \"object\") {\n const defaultProps = node.type[\"props\"] as Record;\n const defaultProp = defaultProps[propName];\n\n if (defaultProp && typeof defaultProp === \"object\" && defaultProp[\"default\"] !== undefined) {\n return defaultProp[\"default\"] as T;\n }\n }\n\n return undefined;\n}\n\n/**\n * Retrieves all prop values from a VNode object. First all default values\n * from the component are retrieved. Then any specified on the DOM will be used\n * to override those default values.\n *\n * @param node The node whose property values are being requested.\n *\n * @returns An object that contains all props and values for the node.\n */\nexport function getVNodeProps(node: VNode): Record {\n const props: Record = {};\n\n // Get all default values from the backing component's defined props.\n if (typeof node.type === \"object\" && typeof node.type[\"props\"] === \"object\") {\n const defaultProps = node.type[\"props\"] as Record;\n\n for (const p in defaultProps) {\n const defaultProp = defaultProps[p];\n\n if (defaultProp && typeof defaultProp === \"object\" && defaultProp[\"default\"] !== undefined) {\n props[p] = defaultProp[\"default\"];\n }\n }\n }\n\n // Override with any values specified on the DOM declaration.\n if (node.props) {\n for (const p in node.props) {\n if (typeof node.type === \"object\" && typeof node.type[\"props\"] === \"object\") {\n const propType = node.type[\"props\"][p]?.type;\n\n if (propType === Boolean) {\n props[p] = node.props[p] === true || node.props[p] === \"\";\n }\n else if (propType === Number) {\n props[p] = toNumberOrNull(node.props[p]) ?? undefined;\n }\n else {\n props[p] = node.props[p];\n }\n }\n else {\n props[p] = node.props[p];\n }\n }\n }\n\n return props;\n}\n\n/**\n * Renders the node into an off-screen div and then extracts the text content\n * by way of the innerText property of the div.\n *\n * @param node The node or component to be rendered.\n * @param props The properties to be passed to the component when it is mounted.\n *\n * @returns The text content of the node after it has rendered.\n */\nexport function extractText(node: VNode | Component, props?: Record): string {\n const el = document.createElement(\"div\");\n\n // Create a new virtual node with the specified properties.\n const vnode = createVNode(node, props);\n\n // Mount the node in our off-screen container.\n render(vnode, el);\n\n const text = el.innerText;\n\n // Unmount it.\n render(null, el);\n\n return text.trim();\n}\n\n/**\n * Renders the node into an off-screen div and then extracts the HTML content\n * by way of the innerHTML property of the div.\n *\n * @param node The node or component to be rendered.\n * @param props The properties to be passed to the component when it is mounted.\n *\n * @returns The HTML content of the node after it has rendered.\n */\nexport function extractHtml(node: VNode | Component, props?: Record): string {\n const el = document.createElement(\"div\");\n\n // Create a new virtual node with the specified properties.\n const vnode = createVNode(node, props);\n\n // Mount the node in our off-screen container.\n render(vnode, el);\n\n const html = el.innerHTML;\n\n // Unmount it.\n render(null, el);\n\n return html;\n}\n\n// #endregion\n","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n//\n\nimport { toNumberOrNull, zeroPad } from \"./numberUtils\";\nconst dateKeyLength = \"YYYYMMDD\".length;\nconst dateKeyNoYearLength = \"MMDD\".length;\n\n/**\n * Gets the year value from the date key.\n * Ex: 20210228 => 2021\n * @param dateKey\n */\nexport function getYear(dateKey: string | null): number {\n const defaultValue = 0;\n\n if (!dateKey || dateKey.length !== dateKeyLength) {\n return defaultValue;\n }\n\n const asString = dateKey.substring(0, 4);\n const year = toNumberOrNull(asString) || defaultValue;\n return year;\n}\n\n/**\n * Gets the month value from the date key.\n * Ex: 20210228 => 2\n * @param dateKey\n */\nexport function getMonth(dateKey: string | null): number {\n const defaultValue = 0;\n\n if (!dateKey) {\n return defaultValue;\n }\n\n if (dateKey.length === dateKeyLength) {\n const asString = dateKey.substring(4, 6);\n return toNumberOrNull(asString) || defaultValue;\n }\n\n if (dateKey.length === dateKeyNoYearLength) {\n const asString = dateKey.substring(0, 2);\n return toNumberOrNull(asString) || defaultValue;\n }\n\n return defaultValue;\n}\n\n/**\n * Gets the day value from the date key.\n * Ex: 20210228 => 28\n * @param dateKey\n */\nexport function getDay(dateKey: string | null): number {\n const defaultValue = 0;\n\n if (!dateKey) {\n return defaultValue;\n }\n\n if (dateKey.length === dateKeyLength) {\n const asString = dateKey.substring(6, 8);\n return toNumberOrNull(asString) || defaultValue;\n }\n\n if (dateKey.length === dateKeyNoYearLength) {\n const asString = dateKey.substring(2, 4);\n return toNumberOrNull(asString) || defaultValue;\n }\n\n return defaultValue;\n}\n\n/**\n * Gets the datekey constructed from the parts.\n * Ex: (2021, 2, 28) => '20210228'\n * @param year\n * @param month\n * @param day\n */\nexport function toDateKey(year: number | null, month: number | null, day: number | null): string {\n if (!year || year > 9999 || year < 0) {\n year = 0;\n }\n\n if (!month || month > 12 || month < 0) {\n month = 0;\n }\n\n if (!day || day > 31 || day < 0) {\n day = 0;\n }\n\n const yearStr = zeroPad(year, 4);\n const monthStr = zeroPad(month, 2);\n const dayStr = zeroPad(day, 2);\n\n return `${yearStr}${monthStr}${dayStr}`;\n}\n\n/**\n * Gets the datekey constructed from the parts.\n * Ex: (2, 28) => '0228'\n * @param month\n * @param day\n */\nexport function toNoYearDateKey(month: number | null, day: number | null): string {\n if (!month || month > 12 || month < 0) {\n month = 0;\n }\n\n if (!day || day > 31 || day < 0) {\n day = 0;\n }\n\n const monthStr = zeroPad(month, 2);\n const dayStr = zeroPad(day, 2);\n\n return `${monthStr}${dayStr}`;\n}\n\nexport default {\n getYear,\n getMonth,\n getDay,\n toDateKey,\n toNoYearDateKey\n};\n","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n//\n\nimport { Guid } from \"@Obsidian/Types\";\nimport { CurrentPersonBag } from \"@Obsidian/ViewModels/Crm/currentPersonBag\";\n\nexport type PageConfig = {\n executionStartTime: number;\n pageId: number;\n pageGuid: Guid;\n pageParameters: Record;\n interactionGuid: Guid;\n currentPerson: CurrentPersonBag | null;\n isAnonymousVisitor: boolean;\n loginUrlWithReturnUrl: string;\n};\n\nexport function smoothScrollToTop(): void {\n window.scrollTo({ top: 0, behavior: \"smooth\" });\n}\n\nexport default {\n smoothScrollToTop\n};\n\n// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-explicit-any\ndeclare const Obsidian: any;\n\n\n/*\n * Code to handle working with modals.\n */\nlet currentModalCount = 0;\n\n/**\n * Track a modal being opened or closed. This is used to adjust the page in response\n * to any modals being visible.\n *\n * @param state true if the modal is now open, false if it is now closed.\n */\nexport function trackModalState(state: boolean): void {\n const body = document.body;\n const cssClasses = [\"modal-open\"];\n\n if (state) {\n currentModalCount++;\n }\n else {\n currentModalCount = currentModalCount > 0 ? currentModalCount - 1 : 0;\n }\n\n if (currentModalCount > 0) {\n for (const cssClass of cssClasses) {\n body.classList.add(cssClass);\n }\n }\n else {\n for (const cssClass of cssClasses) {\n body.classList.remove(cssClass);\n }\n }\n}\n\n/**\n * Loads a JavaScript file asynchronously into the document and returns a\n * Promise that can be used to determine when the script has loaded. The\n * promise will return true if the script loaded successfully or false if it\n * failed to load.\n *\n * The function passed in isScriptLoaded will be called before the script is\n * inserted into the DOM as well as after to make sure it actually loaded.\n *\n * @param source The source URL of the script to be loaded.\n * @param isScriptLoaded An optional function to call to determine if the script is loaded.\n * @param attributes An optional set of attributes to apply to the script tag.\n * @param fingerprint If set to false, then a fingerprint will not be added to the source URL. Default is true.\n *\n * @returns A Promise that indicates if the script was loaded or not.\n */\nexport async function loadJavaScriptAsync(source: string, isScriptLoaded?: () => boolean, attributes?: Record, fingerprint?: boolean): Promise {\n let src = source;\n\n // Add the cache busting fingerprint if we have one.\n if (fingerprint !== false && typeof Obsidian !== \"undefined\" && Obsidian?.options?.fingerprint) {\n if (src.indexOf(\"?\") === -1) {\n src += `?${Obsidian.options.fingerprint}`;\n }\n else {\n src += `&${Obsidian.options.fingerprint}`;\n }\n }\n\n // Check if the script is already loaded. First see if we have a custom\n // function that will do the check. Otherwise fall back to looking for any\n // script tags that have the same source.\n if (isScriptLoaded) {\n if (isScriptLoaded()) {\n return true;\n }\n }\n\n // Make sure the script wasn't already added in some other way.\n const scripts = Array.from(document.getElementsByTagName(\"script\"));\n const thisScript = scripts.filter(s => s.src === src);\n\n if (thisScript.length > 0) {\n const promise = scriptLoadedPromise(thisScript[0]);\n return promise;\n }\n\n // Build the script tag that will be dynamically loaded.\n const script = document.createElement(\"script\");\n script.type = \"text/javascript\";\n script.src = src;\n if (attributes) {\n for (const key in attributes) {\n script.setAttribute(key, attributes[key]);\n }\n }\n\n // Load the script.\n const promise = scriptLoadedPromise(script);\n document.getElementsByTagName(\"head\")[0].appendChild(script);\n\n return promise;\n\n async function scriptLoadedPromise(scriptElement: HTMLScriptElement): Promise {\n try {\n await new Promise((resolve, reject) => {\n scriptElement.addEventListener(\"load\", () => resolve());\n scriptElement.addEventListener(\"error\", () => {\n reject();\n });\n });\n\n // If we have a custom function, call it to see if the script loaded correctly.\n if (isScriptLoaded) {\n return isScriptLoaded();\n }\n\n return true;\n }\n catch {\n return false;\n }\n }\n}\n\n/**\n * Adds a new link to the quick return action menu. The URL in the address bar\n * will be used as the destination.\n *\n * @param title The title of the quick link that identifies the current page.\n * @param section The section title to place this link into.\n * @param sectionOrder The priority order to give the section if it doesn't already exist.\n */\nexport function addQuickReturn(title: string, section: string, sectionOrder?: number): void {\n interface IRock {\n personalLinks: {\n addQuickReturn: (type: string, typeOrder: number, itemName: string) => void\n }\n }\n\n const rock = window[\"Rock\"] as IRock;\n if (rock && rock.personalLinks) {\n rock.personalLinks.addQuickReturn(section, sectionOrder ?? 0, title);\n }\n}\n","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n//\n\nimport { Guid } from \"@Obsidian/Types\";\nimport { ICancellationToken } from \"./cancellation\";\nimport { trackModalState } from \"./page\";\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/naming-convention\ndeclare const Rock: any;\n\n/** The options that describe the dialog. */\nexport type DialogOptions = {\n /** The text to display inside the dialog. */\n message: string;\n\n /** A list of buttons to display, rendered left to right. */\n buttons: ButtonOptions[];\n\n /**\n * An optional container element for the dialog. If not specified then one\n * will be chosen automatically.\n */\n container?: string | Element;\n\n /**\n * An optional cancellation token that will dismiss the dialog automatically\n * and return `cancel` as the button clicked.\n */\n cancellationToken?: ICancellationToken;\n};\n\n/** The options that describe a single button in the dialog. */\nexport type ButtonOptions = {\n /** The key that uniquely identifies this button. */\n key: string;\n\n /** The text to display in the button. */\n label: string;\n\n /** The CSS classes to assign to the button, such as `btn btn-primary`. */\n className: string;\n};\n\n/**\n * Creates a dialog to display a message.\n *\n * @param body The body content to put in the dialog.\n * @param footer The footer content to put in the dialog.\n *\n * @returns An element that should be added to the body.\n */\nfunction createDialog(body: HTMLElement | HTMLElement[], footer: HTMLElement | HTMLElement[] | undefined): HTMLElement {\n // Create the scrollable container that will act as a backdrop for the dialog.\n const scrollable = document.createElement(\"div\");\n scrollable.classList.add(\"modal-scrollable\");\n scrollable.style.zIndex = \"1060\";\n\n // Create the modal that will act as a container for the outer content.\n const modal = document.createElement(\"div\");\n scrollable.appendChild(modal);\n modal.classList.add(\"modal\", \"fade\");\n modal.tabIndex = -1;\n modal.setAttribute(\"role\", \"dialog\");\n modal.setAttribute(\"aria-hidden\", \"false\");\n modal.style.display = \"block\";\n\n // Create the inner dialog of the modal.\n const modalDialog = document.createElement(\"div\");\n modal.appendChild(modalDialog);\n modalDialog.classList.add(\"modal-dialog\");\n\n // Create the container for the inner content.\n const modalContent = document.createElement(\"div\");\n modalDialog.appendChild(modalContent);\n modalContent.classList.add(\"modal-content\");\n\n // Create the container for the body content.\n const modalBody = document.createElement(\"div\");\n modalContent.appendChild(modalBody);\n modalBody.classList.add(\"modal-body\");\n\n // Add all the body elements to the body.\n if (Array.isArray(body)) {\n for (const el of body) {\n modalBody.appendChild(el);\n }\n }\n else {\n modalBody.appendChild(body);\n }\n\n // If we have any footer content then create a footer.\n if (footer && (!Array.isArray(footer) || footer.length > 0)) {\n const modalFooter = document.createElement(\"div\");\n modalContent.appendChild(modalFooter);\n modalFooter.classList.add(\"modal-footer\");\n\n // Add all the footer elements to the footer.\n if (Array.isArray(footer)) {\n for (const el of footer) {\n modalFooter.appendChild(el);\n }\n }\n else {\n modalFooter.appendChild(footer);\n }\n }\n\n // Add a click handler to the background so the user gets feedback\n // that they can't just click away from the dialog.\n scrollable.addEventListener(\"click\", () => {\n modal.classList.remove(\"animated\", \"shake\");\n setTimeout(() => {\n modal.classList.add(\"animated\", \"shake\");\n }, 0);\n });\n\n return scrollable;\n}\n\n/**\n * Construct a standard close button to be placed in the dialog.\n *\n * @returns A button element.\n */\nfunction createCloseButton(): HTMLButtonElement {\n const closeButton = document.createElement(\"button\");\n closeButton.classList.add(\"close\");\n closeButton.type = \"button\";\n closeButton.style.marginTop = \"-10px\";\n closeButton.innerHTML = \"×\";\n\n return closeButton;\n}\n\n/**\n * Creates a standard backdrop element to be placed in the window.\n *\n * @returns An element to show that the background is not active.\n */\nfunction createBackdrop(): HTMLElement {\n const backdrop = document.createElement(\"div\");\n backdrop.classList.add(\"modal-backdrop\");\n backdrop.style.zIndex = \"1050\";\n\n return backdrop;\n}\n\n/**\n * Shows a dialog modal. This is meant to look and behave like the standard\n * Rock.dialog.* functions, but this handles fullscreen mode whereas the old\n * methods do not.\n *\n * @param options The options that describe the dialog to be shown.\n *\n * @returns The key of the button that was clicked, or \"cancel\" if the cancel button was clicked.\n */\nexport function showDialog(options: DialogOptions): Promise {\n return new Promise(resolve => {\n let timer: NodeJS.Timeout | null = null;\n const container = document.fullscreenElement || document.body;\n const body = document.createElement(\"div\");\n body.innerText = options.message;\n\n const buttons: HTMLElement[] = [];\n\n /**\n * Internal function to handle clearing the dialog and resolving the\n * promise.\n *\n * @param result The result to return in the promise.\n */\n function clearDialog(result: string): void {\n // This acts as a way to ensure only a single clear request happens.\n if (timer !== null) {\n return;\n }\n\n // The timout is used as a fallback in case we don't get the\n // transition end event.\n timer = setTimeout(() => {\n backdrop.remove();\n dialog.remove();\n trackModalState(false);\n\n resolve(result);\n }, 1000);\n\n modal.addEventListener(\"transitionend\", () => {\n if (timer) {\n clearTimeout(timer);\n }\n\n backdrop.remove();\n dialog.remove();\n trackModalState(false);\n\n resolve(result);\n });\n\n modal.classList.remove(\"in\");\n backdrop.classList.remove(\"in\");\n }\n\n // Add in all the buttons specified.\n for (const button of options.buttons) {\n const btn = document.createElement(\"button\");\n btn.classList.value = button.className;\n btn.type = \"button\";\n btn.innerText = button.label;\n btn.addEventListener(\"click\", () => {\n clearDialog(button.key);\n });\n buttons.push(btn);\n }\n\n // Construct the close (cancel) button.\n const closeButton = createCloseButton();\n closeButton.addEventListener(\"click\", () => {\n clearDialog(\"cancel\");\n });\n\n const dialog = createDialog([closeButton, body], buttons);\n const backdrop = createBackdrop();\n\n const modal = dialog.querySelector(\".modal\") as HTMLElement;\n\n // Do final adjustments to the elements and add to the body.\n trackModalState(true);\n container.appendChild(dialog);\n container.appendChild(backdrop);\n modal.style.marginTop = `-${modal.offsetHeight / 2.0}px`;\n\n // Show the backdrop and the modal.\n backdrop.classList.add(\"in\");\n modal.classList.add(\"in\");\n\n // Handle dismissal of the dialog by cancellation token.\n options.cancellationToken?.onCancellationRequested(() => {\n clearDialog(\"cancel\");\n });\n });\n}\n\n/**\n * Shows an alert message that requires the user to acknowledge.\n *\n * @param message The message text to be displayed.\n *\n * @returns A promise that indicates when the dialog has been dismissed.\n */\nexport async function alert(message: string): Promise {\n await showDialog({\n message,\n buttons: [\n {\n key: \"ok\",\n label: \"OK\",\n className: \"btn btn-primary\"\n }\n ]\n });\n}\n\n/**\n * Shows a confirmation dialog that consists of OK and Cancel buttons. The\n * user will be required to click one of these two buttons.\n *\n * @param message The message to be displayed inside the dialog.\n *\n * @returns A promise that indicates when the dialog has been dismissed. The\n * value will be true if the OK button was clicked or false otherwise.\n */\nexport async function confirm(message: string): Promise {\n const result = await showDialog({\n message,\n buttons: [\n {\n key: \"ok\",\n label: \"OK\",\n className: \"btn btn-primary\"\n },\n {\n key: \"cancel\",\n label: \"Cancel\",\n className: \"btn btn-default\"\n }\n ]\n });\n\n return result === \"ok\";\n}\n\n/**\n * Shows a delete confirmation dialog that consists of OK and Cancel buttons.\n * The user will be required to click one of these two buttons. The message\n * is standardized.\n *\n * @param nameText The name of type that will be deleted.\n *\n * @returns A promise that indicates when the dialog has been dismissed. The\n * value will be true if the OK button was clicked or false otherwise.\n */\nexport function confirmDelete(typeName: string, additionalMessage?: string): Promise {\n let message = `Are you sure you want to delete this ${typeName}?`;\n\n if (additionalMessage) {\n message += ` ${additionalMessage}`;\n }\n\n return confirm(message);\n}\n\n/**\n * Shows the security dialog for the given entity.\n *\n * @param entityTypeIdKey The identifier of the entity's type.\n * @param entityIdKey The identifier of the entity to secure.\n * @param entityTitle The title of the entity. This is used to construct the modal title.\n */\nexport function showSecurity(entityTypeIdKey: Guid | string | number, entityIdKey: Guid | string | number, entityTitle: string = \"Item\"): void {\n Rock.controls.modal.show(undefined, `/Secure/${entityTypeIdKey}/${entityIdKey}?t=Secure ${entityTitle}&pb=&sb=Done`);\n}\n\n/**\n * Shows the child pages for the given page.\n * @param pageId The page identifier\n */\nexport function showChildPages(pageId: Guid | string | number): void {\n Rock.controls.modal.show(undefined, `/pages/${pageId}?t=Child Pages&pb=&sb=Done`);\n}","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n//\n\n/**\n * Is the value a valid email address?\n * @param val\n */\nexport function isEmail(val: unknown): boolean {\n if (typeof val === \"string\") {\n const re = /^(([^<>()[\\]\\\\.,;:\\s@\"]+(\\.[^<>()[\\]\\\\.,;:\\s@\"]+)*)|(\".+\"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$/;\n return re.test(val.toLowerCase());\n }\n\n return false;\n}\n","import { ListItemBag } from \"@Obsidian/ViewModels/Utility/listItemBag\";\n\n/**\n * A function to convert the enums to array of ListItemBag in the frontend.\n *\n * @param description The enum to be converted to an array of listItemBag as a dictionary of value to enum description\n *\n * @returns An array of ListItemBag.\n */\nexport function enumToListItemBag (description: Record): ListItemBag[] {\n const listItemBagList: ListItemBag[] = [];\n for(const property in description) {\n listItemBagList.push({\n text: description[property].toString(),\n value: property.toString()\n });\n }\n return listItemBagList;\n}","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n//\n\nimport { Guid } from \"@Obsidian/Types\";\nimport { isValidGuid, normalize } from \"./guid\";\nimport { IFieldType } from \"@Obsidian/Types/fieldType\";\n\nconst fieldTypeTable: Record = {};\n\n/** Determines how the field type component is being used so it can adapt to different */\nexport type DataEntryMode = \"defaultValue\" | undefined;\n\n/**\n * Register a new field type in the system. This must be called for all field\n * types a plugin registers.\n *\n * @param fieldTypeGuid The unique identifier of the field type.\n * @param fieldType The class instance that will handle the field type.\n */\nexport function registerFieldType(fieldTypeGuid: Guid, fieldType: IFieldType): void {\n const normalizedGuid = normalize(fieldTypeGuid);\n\n if (!isValidGuid(fieldTypeGuid) || normalizedGuid === null) {\n throw \"Invalid guid specified when registering field type.\";\n }\n\n if (fieldTypeTable[normalizedGuid] !== undefined) {\n throw \"Invalid attempt to replace existing field type.\";\n }\n\n fieldTypeTable[normalizedGuid] = fieldType;\n}\n\n/**\n * Get the field type handler for a given unique identifier.\n *\n * @param fieldTypeGuid The unique identifier of the field type.\n *\n * @returns The field type instance or null if not found.\n */\nexport function getFieldType(fieldTypeGuid: Guid): IFieldType | null {\n const normalizedGuid = normalize(fieldTypeGuid);\n\n if (normalizedGuid !== null) {\n const field = fieldTypeTable[normalizedGuid];\n\n if (field) {\n return field;\n }\n }\n\n console.warn(`Field type \"${fieldTypeGuid}\" was not found`);\n return null;\n}\n\n","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n//\n\n/**\n * Triggers an automatic download of the data so it can be saved to the\n * filesystem.\n *\n * @param data The data to be downloaded by the browser.\n * @param filename The name of the filename to suggest to the browser.\n */\nexport async function downloadFile(data: Blob, filename: string): Promise {\n // Create the URL that contains the file data.\n const url = URL.createObjectURL(data);\n\n // Create a fake hyperlink to simulate an attempt to download a file.\n const element = document.createElement(\"a\");\n element.innerText = \"Download\";\n element.style.position = \"absolute\";\n element.style.top = \"-100px\";\n element.style.left = \"0\";\n element.href = url;\n element.download = filename;\n document.body.appendChild(element);\n element.click();\n document.body.removeChild(element);\n\n setTimeout(() => URL.revokeObjectURL(url), 100);\n}\n","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n//\n\nimport { inject, provide } from \"vue\";\n\n/** The unique symbol used when injecting the form state. */\nconst formStateSymbol = Symbol();\n\n/**\n * Holds the state of a single form on the page along with any callback methods\n * that can be used to interact with the form.\n */\nexport type FormState = {\n /** The number of submissions the form has had. */\n submitCount: number;\n\n /** Sets the current error for the given field name. A blank error means no error. */\n setError: (id: string, name: string, error: string) => void;\n};\n\n/**\n * Contains the internal form error passed between RockForm and RockValidation.\n *\n * This is an internal type and subject to change at any time.\n */\nexport type FormError = {\n /** The name of the field. */\n name: string;\n\n /** The current error text. */\n text: string;\n};\n\n/**\n * Provides the form state for any child components that need access to it.\n * \n * @param state The state that will be provided to child components.\n */\nexport function provideFormState(state: FormState): void {\n provide(formStateSymbol, state);\n}\n\n/**\n * Makes use of the FormState that was previously provided by a parent component.\n *\n * @returns The form state or undefined if it was not available.\n */\nexport function useFormState(): FormState | undefined {\n return inject(formStateSymbol, undefined);\n}\n","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n//\n\n// Define the browser-specific versions of these functions that older browsers\n// implemented before using the standard API.\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/naming-convention\n interface Document {\n mozCancelFullScreen?: () => Promise;\n webkitExitFullscreen?: () => Promise;\n mozFullScreenElement?: Element;\n webkitFullscreenElement?: Element;\n }\n\n // eslint-disable-next-line @typescript-eslint/naming-convention\n interface HTMLElement {\n mozRequestFullscreen?: () => Promise;\n webkitRequestFullscreen?: () => Promise;\n }\n}\n\n/**\n * Request that the window enter true fullscreen mode for the given element.\n * \n * @param element The element that will be the root of the fullscreen view.\n * @param exitCallback The function to call when leaving fullscreen mode.\n *\n * @returns A promise that indicates when the operation has completed.\n */\nexport async function enterFullscreen(element: HTMLElement, exitCallback?: (() => void)): Promise {\n try {\n if (element.requestFullscreen) {\n await element.requestFullscreen();\n }\n else if (element.mozRequestFullscreen) {\n await element.mozRequestFullscreen();\n }\n else if (element.webkitRequestFullscreen) {\n await element.webkitRequestFullscreen();\n }\n else {\n return false;\n }\n\n element.classList.add(\"is-fullscreen\");\n\n const onFullscreenChange = (): void => {\n element.classList.remove(\"is-fullscreen\");\n\n document.removeEventListener(\"fullscreenchange\", onFullscreenChange);\n document.removeEventListener(\"mozfullscreenchange\", onFullscreenChange);\n document.removeEventListener(\"webkitfullscreenchange\", onFullscreenChange);\n\n if (exitCallback) {\n exitCallback();\n }\n };\n\n document.addEventListener(\"fullscreenchange\", onFullscreenChange);\n document.addEventListener(\"mozfullscreenchange\", onFullscreenChange);\n document.addEventListener(\"webkitfullscreenchange\", onFullscreenChange);\n\n return true;\n }\n catch (ex) {\n console.error(ex);\n return false;\n }\n}\n\n/**\n * Checks if any element is currently in fullscreen mode.\n * \n * @returns True if an element is currently in fullscreen mode in the window; otherwise false.\n */\nexport function isFullscreen(): boolean {\n return !!document.fullscreenElement || !!document.mozFullScreenElement || !!document.webkitFullscreenElement;\n}\n\n/**\n * Manually exits fullscreen mode.\n * \n * @returns True if fullscreen mode was exited; otherwise false.\n */\nexport async function exitFullscreen(): Promise {\n try {\n if (document.exitFullscreen) {\n await document.exitFullscreen();\n }\n else if (document.mozCancelFullScreen) {\n await document.mozCancelFullScreen();\n }\n else if (document.webkitExitFullscreen) {\n document.webkitExitFullscreen();\n }\n else {\n return false;\n }\n\n return true;\n }\n catch (ex) {\n console.error(ex);\n return false;\n }\n}\n","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n//\n\n/* global google */\n\nimport { DrawingMode, Coordinate, ILatLng, ILatLngLiteral } from \"@Obsidian/Types/Controls/geo\";\nimport { GeoPickerSettingsBag } from \"@Obsidian/ViewModels/Rest/Controls/geoPickerSettingsBag\";\nimport { GeoPickerGetSettingsOptionsBag } from \"@Obsidian/ViewModels/Rest/Controls/geoPickerGetSettingsOptionsBag\";\nimport { GeoPickerGoogleMapSettingsBag } from \"@Obsidian/ViewModels/Rest/Controls/geoPickerGoogleMapSettingsBag\";\nimport { emptyGuid } from \"./guid\";\nimport { post } from \"./http\";\nimport { loadJavaScriptAsync } from \"./page\";\n\n/**\n * Converts a LatLng object, \"lat,lng\" coordinate string, or WellKnown \"lng lat\" coordinate string to a Coordinate array\n * @param coord Either a string in \"lat,lng\" format or a LatLng object from Google Maps\n * @param isWellKnown True if is \"lng lat\" format, false if it is \"lat, lng\"\n *\n * @returns Coordinate: Tuple with a Latitude number and Longitude number as the elements\n */\nexport function toCoordinate(coord: string | ILatLng, isWellKnown: boolean = false): Coordinate {\n if (typeof coord == \"string\") {\n // WellKnown string format\n if (isWellKnown) {\n return coord.split(\" \").reverse().map(val => parseFloat(val)) as Coordinate;\n }\n // Google Maps URL string format\n else {\n return coord.split(\",\").map(val => parseFloat(val)) as Coordinate;\n }\n }\n else {\n return [coord.lat(), coord.lng()];\n }\n}\n\n/**\n * Takes a Well Known Text value and converts it into a Coordinate array\n */\nexport function wellKnownToCoordinates(wellKnownText: string, type: DrawingMode): Coordinate[] {\n if (wellKnownText == \"\") {\n return [];\n }\n if (type == \"Point\") {\n // From this format: POINT (-112.130946 33.600114)\n return [toCoordinate(wellKnownText.replace(/(POINT *\\( *)|( *\\) *)/ig, \"\"), true)];\n }\n else {\n // From this format: POLYGON ((-112.157058 33.598563, -112.092341 33.595132, -112.117061 33.608715, -112.124957 33.609286, -112.157058 33.598563))\n return wellKnownText.replace(/(POLYGON *\\(+ *)|( *\\)+ *)/ig, \"\").split(/ *, */).map((coord) => toCoordinate(coord, true));\n }\n}\n\n/**\n * Takes a Well Known Text value and converts it into a Coordinate array\n */\nexport function coordinatesToWellKnown(coordinates: Coordinate[], type: DrawingMode): string {\n if (coordinates.length == 0) {\n return \"\";\n }\n else if (type == \"Point\") {\n return `POINT(${coordinates[0].reverse().join(\" \")})`;\n }\n else {\n // DB doesn't work well with the points of a polygon specified in clockwise order for some reason\n if (isClockwisePolygon(coordinates)) {\n coordinates.reverse();\n }\n\n const coordinateString = coordinates.map(coords => coords.reverse().join(\" \")).join(\", \");\n return `POLYGON((${coordinateString}))`;\n }\n}\n\n/**\n * Takes a Coordinate and uses Geocoding to get nearest address\n */\nexport function nearAddressForCoordinate(coordinate: Coordinate): Promise {\n return new Promise(resolve => {\n // only try if google is loaded\n if (window.google) {\n const geocoder = new google.maps.Geocoder();\n geocoder.geocode({ location: new google.maps.LatLng(...coordinate) }, function (results, status) {\n if (status == google.maps.GeocoderStatus.OK && results?.[0]) {\n resolve(\"near \" + results[0].formatted_address);\n }\n else {\n console.log(\"Geocoder failed due to: \" + status);\n resolve(\"\");\n }\n });\n }\n else {\n resolve(\"\");\n }\n });\n}\n\n/**\n * Takes a Coordinate array and uses Geocoding to get nearest address for the first point\n */\nexport function nearAddressForCoordinates(coordinates: Coordinate[]): Promise {\n if (!coordinates || coordinates.length == 0) {\n return Promise.resolve(\"\");\n }\n return nearAddressForCoordinate(coordinates[0]);\n}\n\n/**\n * Determine whether the polygon's coordinates are drawn in clockwise order\n * Thank you dominoc!\n * http://dominoc925.blogspot.com/2012/03/c-code-to-determine-if-polygon-vertices.html\n */\nexport function isClockwisePolygon(polygon: number[][]): boolean {\n let sum = 0;\n\n for (let i = 0; i < polygon.length - 1; i++) {\n sum += (Math.abs(polygon[i + 1][0]) - Math.abs(polygon[i][0])) * (Math.abs(polygon[i + 1][1]) + Math.abs(polygon[i][1]));\n }\n\n return sum > 0;\n}\n\n/**\n * Download the necessary resources to run the maps and return the map settings from the API\n *\n * @param options Options for which data to get from the API\n *\n * @return Promise with the map settings retrieved from the API\n */\nexport async function loadMapResources(options: GeoPickerGetSettingsOptionsBag = { mapStyleValueGuid: emptyGuid }): Promise {\n const response = await post(\"/api/v2/Controls/GeoPickerGetGoogleMapSettings\", undefined, options);\n const googleMapSettings = response.data ?? {};\n\n let keyParam = \"\";\n\n if (googleMapSettings.googleApiKey) {\n keyParam = `key=${googleMapSettings.googleApiKey}&`;\n }\n\n await loadJavaScriptAsync(`https://maps.googleapis.com/maps/api/js?${keyParam}libraries=drawing,visualization,geometry`, () => typeof (google) != \"undefined\" && typeof (google.maps) != \"undefined\", {}, false);\n\n return googleMapSettings;\n}\n\n/**\n * Creates a ILatLng object\n */\nexport function createLatLng(latOrLatLngOrLatLngLiteral: number | ILatLngLiteral | ILatLng, lngOrNoClampNoWrap?: number | boolean | null, noClampNoWrap?: boolean): ILatLng {\n return new google.maps.LatLng(latOrLatLngOrLatLngLiteral as number, lngOrNoClampNoWrap, noClampNoWrap);\n}","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n//\n\nimport { DayOfWeek, RockDateTime } from \"./rockDateTime\";\nimport { newGuid } from \"./guid\";\nimport { toNumberOrNull } from \"./numberUtils\";\nimport { pluralConditional } from \"./stringUtils\";\n\ntype Frequency = \"DAILY\" | \"WEEKLY\" | \"MONTHLY\";\n\n/**\n * The day of the week and an interval number for that particular day.\n */\nexport type WeekdayNumber = {\n /** The interval number for this day. */\n value: number;\n\n /** The day of the week. */\n day: DayOfWeek;\n};\n\n// Abbreviate nth lookup table.\nconst nthNamesAbbreviated: [number, string][] = [\n [1, \"1st\"],\n [2, \"2nd\"],\n [3, \"3rd\"],\n [4, \"4th\"],\n [-1, \"last\"]\n];\n\n// #region Internal Functions\n\n/**\n * Converts the number to a string and pads the left with zeros to make up\n * the minimum required length.\n *\n * @param value The value to be converted to a string.\n * @param length The minimum required length of the final string.\n *\n * @returns A string that represents the value.\n */\nfunction padZeroLeft(value: number, length: number): string {\n const str = value.toString();\n\n return \"0\".repeat(length - str.length) + str;\n}\n\n/**\n * Get a date-only string that can be used in the iCal format.\n *\n * @param date The date object to be converted to a string.\n *\n * @returns A string that represents only the date portion of the parameter.\n */\nfunction getDateString(date: RockDateTime): string {\n const year = date.year;\n const month = date.month;\n const day = date.day;\n\n return `${year}${padZeroLeft(month, 2)}${padZeroLeft(day, 2)}`;\n}\n\n/**\n * Gets a time-only string that can be used in the iCal format.\n *\n * @param date The date object to be converted to a string.\n *\n * @returns A string that represents only the time portion of the parameter.\n */\nfunction getTimeString(date: RockDateTime): string {\n const hour = date.hour;\n const minute = date.minute;\n const second = date.second;\n\n return `${padZeroLeft(hour, 2)}${padZeroLeft(minute, 2)}${padZeroLeft(second, 2)}`;\n}\n\n/**\n * Gets a date and time string that can be used in the iCal format.\n *\n * @param date The date object to be converted to a string.\n *\n * @returns A string that represents only the date and time of the parameter.\n */\nfunction getDateTimeString(date: RockDateTime): string {\n return `${getDateString(date)}T${getTimeString(date)}`;\n}\n\n/**\n * Gets all the date objects from a range or period string value. This converts\n * from an iCal format into a set of date objects.\n *\n * @param value The string value in iCal format.\n *\n * @returns An array of date objects that represents the range or period value.\n */\nfunction getDatesFromRangeOrPeriod(value: string): RockDateTime[] {\n const segments = value.split(\"/\");\n\n if (segments.length === 0) {\n return [];\n }\n\n const startDate = getDateFromString(segments[0]);\n if (!startDate) {\n return [];\n }\n\n if (segments.length !== 2) {\n return [startDate];\n }\n\n const dates: RockDateTime[] = [];\n\n if (segments[1].startsWith(\"P\")) {\n // Value is a period so we have a start date and then a period marker\n // to tell us how long that date extends.\n const days = getPeriodDurationInDays(segments[1]);\n\n for (let day = 0; day < days; day++) {\n const date = startDate.addDays(day);\n if (date) {\n dates.push(date);\n }\n }\n }\n else {\n // Value is a date range so we have a start date and then an end date\n // and we need to fill in the dates in between.\n const endDate = getDateFromString(segments[1]);\n\n if (endDate !== null) {\n let date = startDate;\n\n while (date <= endDate) {\n dates.push(date);\n date = date.addDays(1);\n }\n }\n }\n\n return dates;\n}\n\n/**\n * Get a date object that only has the date portion filled in from the iCal\n * date string. The time will be set to midnight.\n *\n * @param value An iCal date value.\n *\n * @returns A date object that represents the iCal date value.\n */\nfunction getDateFromString(value: string): RockDateTime | null {\n if (value.length < 8) {\n return null;\n }\n\n const year = parseInt(value.substring(0, 4));\n const month = parseInt(value.substring(4, 6));\n const day = parseInt(value.substring(6, 8));\n\n return RockDateTime.fromParts(year, month, day);\n}\n\n/**\n * Get a date object that has both the date and time filled in from the iCal\n * date string.\n *\n * @param value An iCal date value.\n *\n * @returns A date object that represents the iCal date value.\n */\nfunction getDateTimeFromString(value: string): RockDateTime | null {\n if (value.length < 15 || value[8] !== \"T\") {\n return null;\n }\n\n const year = parseInt(value.substring(0, 4));\n const month = parseInt(value.substring(4, 6));\n const day = parseInt(value.substring(6, 8));\n const hour = parseInt(value.substring(9, 11));\n const minute = parseInt(value.substring(11, 13));\n const second = parseInt(value.substring(13, 15));\n\n return RockDateTime.fromParts(year, month, day, hour, minute, second);\n}\n\n/**\n * Gets an iCal period duration in the number of days.\n *\n * @param period The iCal period definition.\n *\n * @returns The number of days found in the definition.\n */\nfunction getPeriodDurationInDays(period: string): number {\n // These are in a format like P1D, P2W, etc.\n if (!period.startsWith(\"P\")) {\n return 0;\n }\n\n if (period.endsWith(\"D\")) {\n return parseInt(period.substring(1, period.length - 1));\n }\n else if (period.endsWith(\"W\")) {\n return parseInt(period.substring(1, period.length - 1)) * 7;\n }\n\n return 0;\n}\n\n/**\n * Gets the specific recurrence dates from a RDATE iCal value string.\n *\n * @param attributes The attributes that were defined on the RDATE property.\n * @param value The value of the RDATE property.\n *\n * @returns An array of date objects found in the RDATE value.\n */\nfunction getRecurrenceDates(attributes: Record, value: string): RockDateTime[] {\n const recurrenceDates: RockDateTime[] = [];\n const valueParts = value.split(\",\");\n let valueType = attributes[\"VALUE\"];\n\n for (const valuePart of valueParts) {\n if(!valueType) {\n // The value type is unspecified and it could be a PERIOD, DATE-TIME or a DATE.\n // Determine it based on the length and the contents of the valuePart string.\n\n const length = valuePart.length;\n\n if (length === 8) { // Eg: 20240117\n valueType = \"DATE\";\n }\n else if ((length === 15 || length === 16) && valuePart[8] === \"T\") { // Eg: 19980119T020000, 19970714T173000Z\n valueType = \"DATE-TIME\";\n }\n else { // Eg: 20240201/20240202, 20240118/P1D\n valueType = \"PERIOD\";\n }\n }\n\n\n if (valueType === \"PERIOD\") {\n // Values are stored in period format, such as \"20221005/P1D\".\n recurrenceDates.push(...getDatesFromRangeOrPeriod(valuePart));\n }\n else if (valueType === \"DATE\") {\n // Values are date-only values.\n const date = getDateFromString(valuePart);\n if (date) {\n recurrenceDates.push(date);\n }\n }\n else if (valueType === \"DATE-TIME\") {\n // Values are date and time values.\n const date = getDateTimeFromString(valuePart);\n if (date) {\n recurrenceDates.push(date);\n }\n }\n }\n\n return recurrenceDates;\n}\n\n/**\n * Gets the name of the weekday from the iCal abbreviation.\n *\n * @param day The iCal day abbreviation.\n *\n * @returns A string that represents the day name.\n */\nfunction getWeekdayName(day: DayOfWeek): \"Sunday\" | \"Monday\" | \"Tuesday\" | \"Wednesday\" | \"Thursday\" | \"Friday\" | \"Saturday\" | \"Unknown\" {\n if (day === DayOfWeek.Sunday) {\n return \"Sunday\";\n }\n else if (day === DayOfWeek.Monday) {\n return \"Monday\";\n }\n else if (day === DayOfWeek.Tuesday) {\n return \"Tuesday\";\n }\n else if (day === DayOfWeek.Wednesday) {\n return \"Wednesday\";\n }\n else if (day === DayOfWeek.Thursday) {\n return \"Thursday\";\n }\n else if (day === DayOfWeek.Friday) {\n return \"Friday\";\n }\n else if (day === DayOfWeek.Saturday) {\n return \"Saturday\";\n }\n else {\n return \"Unknown\";\n }\n}\n\n/**\n * Checks if the date matches one of the weekday options.\n *\n * @param rockDate The date that must match one of the weekday options.\n * @param days The array of weekdays that the date must match.\n *\n * @returns True if the date matches; otherwise false.\n */\nfunction dateMatchesDays(rockDate: RockDateTime, days: DayOfWeek[]): boolean {\n for (const day of days) {\n if (rockDate.dayOfWeek === day) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Checks if the date matches the specifie day of week and also the offset into\n * the month for that day.\n *\n * @param rockDate The date object to be checked.\n * @param dayOfWeek The day of week the date must be on.\n * @param offsets The offset in week, such as 2 meaning the second 'dayOfWeek' or -1 meaning the last 'dayOfWeek'.\n *\n * @returns True if the date matches the options; otherwise false.\n */\nfunction dateMatchesOffsetDayOfWeeks(rockDate: RockDateTime, dayOfWeek: DayOfWeek, offsets: number[]): boolean {\n if (!dateMatchesDays(rockDate, [dayOfWeek])) {\n return false;\n }\n\n const dayOfMonth = rockDate.day;\n\n for (const offset of offsets) {\n if (offset === 1 && dayOfMonth >= 1 && dayOfMonth <= 7) {\n return true;\n }\n else if (offset === 2 && dayOfMonth >= 8 && dayOfMonth <= 14) {\n return true;\n }\n else if (offset === 3 && dayOfMonth >= 15 && dayOfMonth <= 21) {\n return true;\n }\n else if (offset === 4 && dayOfMonth >= 22 && dayOfMonth <= 28) {\n return true;\n }\n else if (offset === -1) {\n const lastDayOfMonth = rockDate.addDays(-(rockDate.day - 1)).addMonths(1).addDays(-1).day;\n\n if (dayOfMonth >= (lastDayOfMonth - 7) && dayOfMonth <= lastDayOfMonth) {\n return true;\n }\n }\n }\n\n return false;\n}\n\n/**\n * Gets the DayOfWeek value that corresponds to the iCal formatted weekday.\n *\n * @param day The day of the week to be parsed.\n *\n * @returns A DayOfWeek value that represents the day.\n */\nfunction getDayOfWeekFromIcalDay(day: \"SU\" | \"MO\" | \"TU\" | \"WE\" | \"TH\" | \"FR\" | \"SA\"): DayOfWeek {\n switch (day) {\n case \"SU\":\n return DayOfWeek.Sunday;\n\n case \"MO\":\n return DayOfWeek.Monday;\n case \"TU\":\n return DayOfWeek.Tuesday;\n\n case \"WE\":\n return DayOfWeek.Wednesday;\n\n case \"TH\":\n return DayOfWeek.Thursday;\n\n case \"FR\":\n return DayOfWeek.Friday;\n\n case \"SA\":\n return DayOfWeek.Saturday;\n }\n}\n\n/**\n * Gets the iCal abbreviation for the day of the week.\n *\n * @param day The day of the week to be converted to iCal format.\n *\n * @returns An iCal representation of the day of week.\n */\nfunction getiCalDay(day: DayOfWeek): \"SU\" | \"MO\" | \"TU\" | \"WE\" | \"TH\" | \"FR\" | \"SA\" {\n switch (day) {\n case DayOfWeek.Sunday:\n return \"SU\";\n\n case DayOfWeek.Monday:\n return \"MO\";\n\n case DayOfWeek.Tuesday:\n return \"TU\";\n\n case DayOfWeek.Wednesday:\n return \"WE\";\n\n case DayOfWeek.Thursday:\n return \"TH\";\n\n case DayOfWeek.Friday:\n return \"FR\";\n\n case DayOfWeek.Saturday:\n return \"SA\";\n }\n}\n\n/**\n * Normalizes line length so that none of the individual lines exceed the\n * maximum length of 75 charactes from the RFC.\n *\n * @param lines The array of lines to be normalized.\n *\n * @returns A new array with the lines normalized for length.\n */\nfunction normalizeLineLength(lines: string[]): string[] {\n const newLines: string[] = [...lines];\n\n for (let lineNumber = 0; lineNumber < newLines.length; lineNumber++) {\n // Spec does not allow lines longer than 75 characters.\n if (newLines[lineNumber].length > 75) {\n const currentLine = newLines[lineNumber].substring(0, 75);\n const newLine = \" \" + newLines[lineNumber].substring(75);\n\n newLines.splice(lineNumber, 1, currentLine, newLine);\n }\n }\n\n return newLines;\n}\n\n/**\n * Denormalizes line length so that any continuation lines are appending\n * to the previous line for proper parsing.\n *\n * @param lines The array of lines to be denormalized.\n *\n * @returns A new array with the lines denormalized.\n */\nfunction denormalizeLineLength(lines: string[]): string[] {\n const newLines: string[] = [...lines];\n\n for (let lineNumber = 1; lineNumber < newLines.length;) {\n if (newLines[lineNumber][0] === \" \") {\n newLines[lineNumber - 1] += newLines[lineNumber].substring(1);\n newLines.splice(lineNumber, 1);\n }\n else {\n lineNumber++;\n }\n }\n\n return newLines;\n}\n\n// #endregion\n\n/**\n * Helper utility to feed lines into ICS parsers.\n */\nclass LineFeeder {\n // #region Properties\n\n /**\n * The denormalzied lines that represent the ICS data.\n */\n private lines: string[];\n\n // #endregion\n\n // #region Constructors\n\n /**\n * Creates a new LineFeeder with the given content.\n *\n * @param content A string that represents raw ICS data.\n */\n constructor(content: string) {\n const lines = content.split(/\\r\\n|\\n|\\r/);\n\n this.lines = denormalizeLineLength(lines);\n }\n\n // #endregion\n\n // #region Functions\n\n /**\n * Peek at the next line to be read from the feeder.\n *\n * @returns The next line to be read or null if no more lines remain.\n */\n public peek(): string | null {\n if (this.lines.length === 0) {\n return null;\n }\n\n return this.lines[0];\n }\n\n /**\n * Pops the next line from the feeder, removing it.\n *\n * @returns The line that was removed from the feeder or null if no lines remain.\n */\n public pop(): string | null {\n if (this.lines.length === 0) {\n return null;\n }\n\n return this.lines.splice(0, 1)[0];\n }\n\n // #endregion\n}\n\n/**\n * Logic and structure for a rule that defines when an even recurs on\n * different dates.\n */\nexport class RecurrenceRule {\n // #region Properties\n\n /**\n * The frequency of this recurrence. Only Daily, Weekly and Monthly\n * are supported.\n */\n public frequency?: Frequency;\n\n /**\n * The date at which no more event dates will be generated. This is\n * an exclusive date, meaning if an event date lands on this date\n * then it will not be included in the list of dates.\n */\n public endDate?: RockDateTime;\n\n /**\n * The maximum number of dates, including the original date, that\n * should be generated.\n */\n public count?: number;\n\n /**\n * The interval between dates based on the frequency. If this value is\n * 2 and frequency is Weekly, then you are asking for \"every other week\".\n */\n public interval: number = 1;\n\n /**\n * The days of the month the event should recur on. Only a single value\n * is supported currently.\n */\n public byMonthDay: number[] = [];\n\n /**\n * The days of the week the event shoudl recur on.\n */\n public byDay: WeekdayNumber[] = [];\n\n // #endregion\n\n // #region Constructors\n\n /**\n * Creates a new recurrence rule that can be used to define or adjust the\n * recurrence pattern of an event.\n *\n * @param rule An existing RRULE string from an iCal file.\n *\n * @returns A new instance that can be used to adjust or define the rule.\n */\n public constructor(rule: string | undefined = undefined) {\n if (!rule) {\n return;\n }\n\n // Rule has a format like \"FREQ=DAILY;COUNT=5\" so we split by semicolon\n // first and then sub-split by equals character and then stuff everything\n // into this values object.\n const values: Record = {};\n\n for (const attr of rule.split(\";\")) {\n const attrParts = attr.split(\"=\");\n if (attrParts.length === 2) {\n values[attrParts[0]] = attrParts[1];\n }\n }\n\n // Make sure the values we have are valid.\n if (values[\"UNTIL\"] !== undefined && values[\"COUNT\"] !== undefined) {\n throw new Error(`Recurrence rule '${rule}' cannot specify both UNTIL and COUNT.`);\n }\n\n if (values[\"FREQ\"] !== \"DAILY\" && values[\"FREQ\"] !== \"WEEKLY\" && values[\"FREQ\"] !== \"MONTHLY\") {\n throw new Error(`Invalid frequence for recurrence rule '${rule}'.`);\n }\n\n this.frequency = values[\"FREQ\"];\n\n if (values[\"UNTIL\"]?.length === 8) {\n this.endDate = getDateFromString(values[\"UNTIL\"]) ?? undefined;\n }\n else if (values[\"UNTIL\"]?.length >= 15) {\n this.endDate = getDateTimeFromString(values[\"UNTIL\"]) ?? undefined;\n }\n\n if (values[\"COUNT\"] !== undefined) {\n this.count = toNumberOrNull(values[\"COUNT\"]) ?? undefined;\n }\n\n if (values[\"INTERVAL\"] !== undefined) {\n this.interval = toNumberOrNull(values[\"INTERVAL\"]) ?? 1;\n }\n\n if (values[\"BYMONTHDAY\"] !== undefined && values[\"BYMONTHDAY\"].length > 0) {\n this.byMonthDay = [];\n\n for (const v of values[\"BYMONTHDAY\"].split(\",\")) {\n const num = toNumberOrNull(v);\n if (num !== null) {\n this.byMonthDay.push(num);\n }\n }\n }\n\n if (values[\"BYDAY\"] !== undefined && values[\"BYDAY\"].length > 0) {\n this.byDay = [];\n\n for (const v of values[\"BYDAY\"].split(\",\")) {\n if (v.length < 2) {\n continue;\n }\n\n const num = v.length > 2 ? toNumberOrNull(v.substring(0, v.length - 2)) : 1;\n const day = v.substring(v.length - 2);\n\n if (num === null) {\n continue;\n }\n\n if (day === \"SU\" || day === \"MO\" || day === \"TU\" || day == \"WE\" || day == \"TH\" || day == \"FR\" || day == \"SA\") {\n this.byDay.push({\n value: num,\n day: getDayOfWeekFromIcalDay(day)\n });\n }\n }\n }\n }\n\n // #endregion\n\n // #region Functions\n\n /**\n * Builds and returns the RRULE value for an iCal file export.\n *\n * @returns A RRULE value that represents the recurrence rule.\n */\n public build(): string {\n const attributes: string[] = [];\n\n attributes.push(`FREQ=${this.frequency}`);\n\n if (this.count !== undefined) {\n attributes.push(`COUNT=${this.count}`);\n }\n else if (this.endDate) {\n attributes.push(`UNTIL=${getDateTimeString(this.endDate)}`);\n }\n\n if (this.interval > 1) {\n attributes.push(`INTERVAL=${this.interval}`);\n }\n\n if (this.byMonthDay.length > 0) {\n const monthDayValues = this.byMonthDay.map(md => md.toString()).join(\",\");\n attributes.push(`BYMONTHDAY=${monthDayValues}`);\n }\n\n if (this.frequency === \"MONTHLY\" && this.byDay.length > 0) {\n const dayValues = this.byDay.map(d => `${d.value}${getiCalDay(d.day)}`);\n attributes.push(`BYDAY=${dayValues}`);\n }\n else if (this.byDay.length > 0) {\n const dayValues = this.byDay.map(d => d.value !== 1 ? `${d.value}${getiCalDay(d.day)}` : getiCalDay(d.day));\n attributes.push(`BYDAY=${dayValues}`);\n }\n\n return attributes.join(\";\");\n }\n\n /**\n * Gets all the dates within the range that match the recurrence rule. A\n * maximum of 100,000 dates will be returned by this function.\n *\n * @param eventStartDateTime The start date and time of the primary event this rule is for.\n * @param startDateTime The inclusive starting date and time that events should be returned for.\n * @param endDateTime The exclusive ending date and time that events should be returned for.\n *\n * @returns An array of date objects that represent the additional dates and times for the event.\n */\n public getDates(eventStartDateTime: RockDateTime, startDateTime: RockDateTime, endDateTime: RockDateTime): RockDateTime[] {\n const dates: RockDateTime[] = [];\n let rockDate = eventStartDateTime;\n let dateCount = 0;\n\n if (!rockDate) {\n return [];\n }\n\n if (this.endDate && this.endDate < endDateTime) {\n endDateTime = this.endDate;\n }\n\n while (rockDate < endDateTime && dateCount < 100_000) {\n if (this.count && dateCount >= this.count) {\n break;\n }\n\n dateCount++;\n\n if (rockDate >= startDateTime) {\n dates.push(rockDate);\n }\n\n const nextDate = this.nextDateAfter(rockDate);\n\n if (nextDate === null) {\n break;\n }\n else {\n rockDate = nextDate;\n }\n }\n\n return dates;\n }\n\n /**\n * Gets the next valid date after the specified date based on our recurrence\n * rules.\n *\n * @param rockDate The reference date that should be used when calculation the next date.\n *\n * @returns The next date after the reference date or null if one cannot be determined.\n */\n private nextDateAfter(rockDate: RockDateTime): RockDateTime | null {\n if (this.frequency === \"DAILY\") {\n return rockDate.addDays(this.interval);\n }\n else if (this.frequency === \"WEEKLY\" && this.byDay.length > 0) {\n let nextDate = rockDate;\n\n if (nextDate.dayOfWeek === DayOfWeek.Saturday) {\n // On saturday process any skip intervals to move past the next n weeks.\n nextDate = nextDate.addDays(1 + ((this.interval - 1) * 7));\n }\n else {\n nextDate = nextDate.addDays(1);\n }\n\n while (!dateMatchesDays(nextDate, this.byDay.map(d => d.day))) {\n if (nextDate.dayOfWeek === DayOfWeek.Saturday) {\n // On saturday process any skip intervals to move past the next n weeks.\n nextDate = nextDate.addDays(1 + ((this.interval - 1) * 7));\n }\n else {\n nextDate = nextDate.addDays(1);\n }\n }\n\n return nextDate;\n }\n else if (this.frequency === \"MONTHLY\") {\n if (this.byMonthDay.length > 0) {\n let nextDate = rockDate.addDays(-(rockDate.day - 1));\n\n if (rockDate.day >= this.byMonthDay[0]) {\n nextDate = nextDate.addMonths(this.interval);\n }\n\n let lastDayOfMonth = nextDate.addMonths(1).addDays(-1).day;\n let loopCount = 0;\n\n // Skip any months that don't have this day number.\n while (lastDayOfMonth < this.byMonthDay[0]) {\n nextDate = nextDate.addMonths(this.interval);\n\n lastDayOfMonth = nextDate.addMonths(1).addDays(-1).day;\n\n // Fail-safe check so we don't get stuck looping forever\n // if the rule is one that can't be determined. Such as a\n // rule for the 30th day of the month every 12 months\n // starting in February.\n if (loopCount++ >= 100) {\n return null;\n }\n }\n\n nextDate = nextDate.addDays(this.byMonthDay[0] - 1);\n\n return nextDate;\n }\n else if (this.byDay.length > 0) {\n const dayOfWeek = this.byDay[0].day;\n const offsets = this.byDay.map(d => d.value);\n\n let nextDate = rockDate.addDays(1);\n\n while (!dateMatchesOffsetDayOfWeeks(nextDate, dayOfWeek, offsets)) {\n nextDate = nextDate.addDays(1);\n }\n\n return nextDate;\n }\n }\n\n return null;\n }\n\n // #endregion\n}\n\n/**\n * A single event inside a calendar.\n */\nexport class Event {\n // #region Properties\n\n /**\n * The unique identifier for this schedule used in the scheduled event.\n */\n public uid?: string;\n\n /**\n * The first date and time that the event occurs on. This must be provided\n * before the schedule can be built.\n */\n public startDateTime?: RockDateTime;\n\n /**\n * The end date and time for the event. This must be provided before\n * this schedule can be built.\n */\n public endDateTime?: RockDateTime;\n\n /**\n * An array of dates to be excluded from the recurrence rules.\n */\n public excludedDates: RockDateTime[] = [];\n\n /**\n * An array of specific dates that this schedule will recur on. This is\n * only valid if recurrenceRules contains no rules.\n */\n public recurrenceDates: RockDateTime[] = [];\n\n /**\n * The rules that define when this schedule recurs on for additional dates.\n * Only the first rule is currently supported.\n */\n public recurrenceRules: RecurrenceRule[] = [];\n\n // #endregion\n\n // #region Constructors\n\n /**\n * Creates a new internet calendar event.\n *\n * @param icsContent The content from the ICS file that represents this single event.\n *\n * @returns A new Event instance.\n */\n public constructor(icsContent: string | LineFeeder | undefined = undefined) {\n if (icsContent === undefined) {\n this.uid = newGuid();\n return;\n }\n\n let feeder: LineFeeder;\n\n if (typeof icsContent === \"string\") {\n feeder = new LineFeeder(icsContent);\n }\n else {\n feeder = icsContent;\n }\n\n this.parse(feeder);\n }\n\n // #endregion\n\n // #region Functions\n\n /**\n * Build the event as a list of individual lines that make up the event in\n * the ICS file format.\n *\n * @returns An array of lines to be inserted into an ICS file.\n */\n public buildLines(): string[] {\n if (!this.startDateTime || !this.endDateTime) {\n return [];\n }\n\n const lines: string[] = [];\n\n lines.push(\"BEGIN:VEVENT\");\n lines.push(`DTEND:${getDateTimeString(this.endDateTime)}`);\n lines.push(`DTSTAMP:${getDateTimeString(RockDateTime.now())}`);\n lines.push(`DTSTART:${getDateTimeString(this.startDateTime)}`);\n\n if (this.excludedDates.length > 0) {\n lines.push(`EXDATE:${this.excludedDates.map(d => getDateString(d) + \"/P1D\").join(\",\")}`);\n }\n\n if (this.recurrenceDates.length > 0) {\n const recurrenceDates: string[] = [];\n for (const date of this.recurrenceDates) {\n const rDate = RockDateTime.fromParts(date.year, date.month, date.day, this.startDateTime.hour, this.startDateTime.minute, this.startDateTime.second);\n if (rDate) {\n recurrenceDates.push(getDateTimeString(rDate));\n }\n }\n\n lines.push(`RDATE:${recurrenceDates.join(\",\")}`);\n }\n else if (this.recurrenceRules.length > 0) {\n for (const rrule of this.recurrenceRules) {\n lines.push(`RRULE:${rrule.build()}`);\n }\n }\n\n lines.push(\"SEQUENCE:0\");\n lines.push(`UID:${this.uid}`);\n lines.push(\"END:VEVENT\");\n\n return lines;\n }\n\n /**\n * Builds the event into a string that conforms to ICS format.\n *\n * @returns An ICS formatted string that represents the event data.\n */\n public build(): string | null {\n const lines = this.buildLines();\n\n if (lines.length === 0) {\n return null;\n }\n\n return normalizeLineLength(lines).join(\"\\r\\n\");\n }\n\n /**\n * Parse data from an existing event and store it on this instance.\n *\n * @param feeder The feeder that will provide the line data for parsing.\n */\n private parse(feeder: LineFeeder): void {\n let duration: string | null = null;\n let line: string | null;\n\n // Verify this is an event.\n if (feeder.peek() !== \"BEGIN:VEVENT\") {\n throw new Error(\"Invalid event.\");\n }\n\n feeder.pop();\n\n // Parse the line until we run out of lines or see an END line.\n while ((line = feeder.pop()) !== null) {\n if (line === \"END:VEVENT\") {\n break;\n }\n\n const splitAt = line.indexOf(\":\");\n if (splitAt < 0) {\n continue;\n }\n\n let key = line.substring(0, splitAt);\n const value = line.substring(splitAt + 1);\n\n const keyAttributes: Record = {};\n const keySegments = key.split(\";\");\n if (keySegments.length > 1) {\n key = keySegments[0];\n keySegments.splice(0, 1);\n\n for (const attr of keySegments) {\n const attrSegments = attr.split(\"=\");\n if (attr.length === 2) {\n keyAttributes[attrSegments[0]] = attrSegments[1];\n }\n }\n }\n\n if (key === \"DTSTART\") {\n this.startDateTime = getDateTimeFromString(value) ?? undefined;\n }\n else if (key === \"DTEND\") {\n this.endDateTime = getDateTimeFromString(value) ?? undefined;\n }\n else if (key === \"RRULE\") {\n this.recurrenceRules.push(new RecurrenceRule(value));\n }\n else if (key === \"RDATE\") {\n this.recurrenceDates = getRecurrenceDates(keyAttributes, value);\n }\n else if (key === \"UID\") {\n this.uid = value;\n }\n else if (key === \"DURATION\") {\n duration = value;\n }\n else if (key === \"EXDATE\") {\n const dateValues = value.split(\",\");\n for (const dateValue of dateValues) {\n const dates = getDatesFromRangeOrPeriod(dateValue);\n this.excludedDates.push(...dates);\n }\n }\n }\n\n if (duration !== null) {\n // TODO: Calculate number of seconds and add to startDate.\n }\n }\n\n /**\n * Determines if the date is listed in one of the excluded dates. This\n * currently only checks the excludedDates but in the future might also\n * check the excluded rules.\n *\n * @param rockDate The date to be checked to see if it is excluded.\n *\n * @returns True if the date is excluded; otherwise false.\n */\n private isDateExcluded(rockDate: RockDateTime): boolean {\n const rockDateOnly = rockDate.date;\n\n for (const excludedDate of this.excludedDates) {\n if (excludedDate.date.isEqualTo(rockDateOnly)) {\n return true;\n }\n }\n\n return false;\n }\n\n /**\n * Get all the dates for this event that fall within the specified date range.\n *\n * @param startDateTime The inclusive starting date to use when filtering event dates.\n * @param endDateTime The exclusive endign date to use when filtering event dates.\n *\n * @returns An array of dates that fall between startDateTime and endDateTime.\n */\n public getDates(startDateTime: RockDateTime, endDateTime: RockDateTime): RockDateTime[] {\n if (!this.startDateTime) {\n return [];\n }\n\n // If the schedule has a startDateTime that is later than the requested\n // startDateTime then use ours instead.\n if (this.startDateTime > startDateTime) {\n startDateTime = this.startDateTime;\n }\n\n if (this.recurrenceDates.length > 0) {\n const dates: RockDateTime[] = [];\n const recurrenceDates: RockDateTime[] = [this.startDateTime, ...this.recurrenceDates];\n\n for (const date of recurrenceDates) {\n if (date >= startDateTime && date < endDateTime) {\n dates.push(date);\n }\n }\n\n return dates;\n }\n else if (this.recurrenceRules.length > 0) {\n const rrule = this.recurrenceRules[0];\n\n return rrule.getDates(this.startDateTime, startDateTime, endDateTime)\n .filter(d => !this.isDateExcluded(d));\n }\n else {\n if (this.startDateTime >= startDateTime && this.startDateTime < endDateTime) {\n return [this.startDateTime];\n }\n\n return [];\n }\n }\n\n /**\n * Get the friendly text string that represents this event. This will be a\n * plain text string with no formatting applied.\n *\n * @returns A string that represents the event in a human friendly manner.\n */\n public toFriendlyText(): string {\n return this.toFriendlyString(false);\n }\n\n /**\n * Get the friendly HTML string that represents this event. This will be\n * formatted with HTML to make the information easier to read.\n *\n * @returns A string that represents the event in a human friendly manner.\n */\n public toFriendlyHtml(): string {\n return this.toFriendlyString(true);\n }\n\n /**\n * Get the friendly string that can be easily understood by a human.\n *\n * @param html If true then the string can contain HTML content to make things easier to read.\n *\n * @returns A string that represents the event in a human friendly manner.\n */\n private toFriendlyString(html: boolean): string {\n if (!this.startDateTime) {\n return \"\";\n }\n\n const startTimeText = this.startDateTime.toLocaleString({ hour: \"numeric\", minute: \"2-digit\", hour12: true });\n\n if (this.recurrenceRules.length > 0) {\n const rrule = this.recurrenceRules[0];\n\n if (rrule.frequency === \"DAILY\") {\n let result = \"Daily\";\n\n if (rrule.interval > 1) {\n result += ` every ${rrule.interval} ${pluralConditional(rrule.interval, \"day\", \"days\")}`;\n }\n\n result += ` at ${startTimeText}`;\n\n return result;\n }\n else if (rrule.frequency === \"WEEKLY\") {\n if (rrule.byDay.length === 0) {\n return \"No Scheduled Days\";\n }\n\n let result = rrule.byDay.map(d => getWeekdayName(d.day) + \"s\").join(\",\");\n\n if (rrule.interval > 1) {\n result = `Every ${rrule.interval} weeks: ${result}`;\n }\n else {\n result = `Weekly: ${result}`;\n }\n\n return `${result} at ${startTimeText}`;\n }\n else if (rrule.frequency === \"MONTHLY\") {\n if (rrule.byMonthDay.length > 0) {\n let result = `Day ${rrule.byMonthDay[0]} of every `;\n\n if (rrule.interval > 1) {\n result += `${rrule.interval} months`;\n }\n else {\n result += \"month\";\n }\n\n return `${result} at ${startTimeText}`;\n }\n else if (rrule.byDay.length > 0) {\n const byDay = rrule.byDay[0];\n const offsetNames = nthNamesAbbreviated.filter(n => rrule.byDay.some(d => d.value == n[0])).map(n => n[1]);\n let result = \"\";\n\n if (offsetNames.length > 0) {\n let nameText: string;\n\n if (offsetNames.length > 2) {\n nameText = `${offsetNames.slice(0, offsetNames.length - 1).join(\", \")} and ${offsetNames[offsetNames.length - 1]}`;\n }\n else {\n nameText = offsetNames.join(\" and \");\n }\n result = `The ${nameText} ${getWeekdayName(byDay.day)} of every month`;\n }\n else {\n return \"\";\n }\n\n return `${result} at ${startTimeText}`;\n }\n else {\n return \"\";\n }\n }\n else {\n return \"\";\n }\n }\n else {\n const dates: RockDateTime[] = [this.startDateTime, ...this.recurrenceDates];\n\n if (dates.length === 1) {\n return `Once at ${this.startDateTime.toASPString(\"g\")}`;\n }\n else if (!html || dates.length > 99) {\n const firstDate = dates[0];\n const lastDate = dates[dates.length - 1];\n\n if (firstDate && lastDate) {\n return `Multiple dates between ${firstDate.toASPString(\"g\")} and ${lastDate.toASPString(\"g\")}`;\n }\n else {\n return \"\";\n }\n }\n else if (dates.length > 1) {\n let listHtml = `]`;\n\n for (const date of dates) {\n listHtml += `- ${date.toASPString(\"g\")}
`;\n }\n\n listHtml += \"\";\n\n return listHtml;\n }\n else {\n return \"No Schedule\";\n }\n }\n }\n\n // #endregion\n}\n\n/**\n * A recurring schedule allows schedules to be built and customized from the iCal\n * format used in ics files.\n */\nexport class Calendar {\n // #region Properties\n\n /**\n * The events that exist for this calendar.\n */\n public events: Event[] = [];\n\n // #endregion\n\n // #region Constructors\n\n /**\n * Creates a new Calendar instance.\n *\n * @param icsContent The content from an ICS file to initialize the calendar with.\n *\n * @returns A new Calendar instance.\n */\n public constructor(icsContent: string | undefined = undefined) {\n if (icsContent === undefined) {\n return;\n }\n\n const feeder = new LineFeeder(icsContent);\n\n this.parse(feeder);\n }\n\n // #endregion\n\n // #region Functions\n\n /**\n * Builds the calendar into a string that conforms to ICS format.\n *\n * @returns An ICS formatted string that represents the calendar data.\n */\n public build(): string | null {\n const lines: string[] = [];\n\n lines.push(\"BEGIN:VCALENDAR\");\n lines.push(\"PRODID:-//github.com/SparkDevNetwork/Rock//NONSGML Rock//EN\");\n lines.push(\"VERSION:2.0\");\n\n for (const event of this.events) {\n lines.push(...event.buildLines());\n }\n\n lines.push(\"END:VCALENDAR\");\n\n return denormalizeLineLength(lines).join(\"\\r\\n\");\n }\n\n /**\n * Parses the ICS data from a line feeder and constructs the calendar\n * from that data.\n *\n * @param feeder The feeder that provides the individual lines.\n */\n private parse(feeder: LineFeeder): void {\n let line: string | null;\n\n // Parse the line data.\n while ((line = feeder.peek()) !== null) {\n if (line === \"BEGIN:VEVENT\") {\n const event = new Event(feeder);\n\n this.events.push(event);\n }\n else {\n feeder.pop();\n }\n }\n }\n\n // #endregion\n}\n","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n//\n\nimport { Liquid } from \"@Obsidian/Libs/liquidjs\";\n\nconst engine = new Liquid({\n cache: true\n});\n\nexport function resolveMergeFields(template: string, mergeFields: Record): string {\n const tpl = engine.parse(template);\n\n return engine.renderSync(tpl, mergeFields);\n}\n","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n//\n\nimport { ListItemBag } from \"@Obsidian/ViewModels/Utility/listItemBag\";\n\nexport function asListItemBagOrNull(bagJson: string): ListItemBag | null {\n try {\n const val = JSON.parse(bagJson);\n\n if (\"value\" in val || \"text\" in val) {\n return val;\n }\n\n return null;\n }\n catch (e) {\n return null;\n }\n}","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n//\n\nimport { MergeFieldPickerFormatSelectedValueOptionsBag } from \"@Obsidian/ViewModels/Rest/Controls/mergeFieldPickerFormatSelectedValueOptionsBag\";\nimport { useHttp } from \"./http\";\n\n/**\n * Take a given mergeFieldPicker value and format it for Lava\n *\n * @param value The merge field to be formatted\n *\n * @returns The formatted string in a Promise\n */\nexport async function formatValue(value: string): Promise {\n const http = useHttp();\n\n const options: MergeFieldPickerFormatSelectedValueOptionsBag = {\n selectedValue: value\n };\n\n const response = await http.post(\"/api/v2/Controls/MergeFieldPickerFormatSelectedValue\", {}, options);\n\n if (response.isSuccess && response.data) {\n return response.data;\n }\n else {\n console.error(\"Error\", response.errorMessage || `Error formatting '${value}'.`);\n return \"\";\n }\n}","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n\nexport function fromEntries(entries: Iterable<[PropertyKey, string]>): Record {\n const res = {};\n for (const entry of entries) {\n res[entry[0]] = entry[1];\n }\n return res;\n}\n\n/**\n * Gets the value at the specified path within the object.\n *\n * @example\n * const object = {\n * person: {\n * name: \"Ted Decker\"\n * }\n * };\n *\n * const value = getValueFromPath(object, \"person.name\"); // returns \"Ted Decker\"\n *\n * @param object The object containing the desired value.\n * @param path The dot-separated path name to the desired value.\n * @returns The value at the specified path within the object, or `undefined`\n * if no such path exists.\n */\nexport function getValueFromPath(object: Record, path: string): unknown {\n if (!object || !path) {\n return;\n }\n\n const pathNames = path.split(\".\");\n\n for (let i = 0; i < pathNames.length; i++) {\n const pathName = pathNames[i].trim();\n\n // If the object doesn't have the specified path name as its own\n // property, return `undefined`.\n if (!pathName || !Object.prototype.hasOwnProperty.call(object, pathName)) {\n return;\n }\n\n const value = object[pathName];\n\n // If this is the last path name specified, return the current value.\n if (i === pathNames.length - 1) {\n return value;\n }\n\n // If the current value is not an object, but there are still\n // more path names to traverse, return `undefined`.\n if (typeof value !== \"object\") {\n return;\n }\n\n // Reassign `object` to the current value. This type assertion might\n // be incorrect, but will be caught on the next iteration if so,\n // in which case `undefined` will be returned.\n object = value as Record;\n }\n\n // If we somehow got here, return `undefined`.\n return;\n}\n","// \n// Copyright by the Spark Development Network\n//\n// Licensed under the Rock Community License (the \"License\");\n// you may not use this file except in compliance with the License.\n// You may obtain a copy of the License at\n//\n// http://www.rockrms.com/license\n//\n// Unless required by applicable law or agreed to in writing, software\n// distributed under the License is distributed on an \"AS IS\" BASIS,\n// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n// See the License for the specific language governing permissions and\n// limitations under the License.\n// \n\nimport Cache from \"./cache\";\nimport { useHttp } from \"./http\";\nimport { PhoneNumberBoxGetConfigurationResultsBag } from \"@Obsidian/ViewModels/Rest/Controls/phoneNumberBoxGetConfigurationResultsBag\";\nimport { PhoneNumberCountryCodeRulesConfigurationBag } from \"@Obsidian/ViewModels/Rest/Controls/phoneNumberCountryCodeRulesConfigurationBag\";\nimport { PhoneNumberBoxGetConfigurationOptionsBag } from \"@Obsidian/ViewModels/Rest/Controls/phoneNumberBoxGetConfigurationOptionsBag\";\n\nconst http = useHttp();\n\n/**\n * Fetch the configuration for phone numbers and their possible formats for different countries\n */\nasync function fetchPhoneNumberConfiguration(): Promise {\n const result = await http.post(\"/api/v2/Controls/PhoneNumberBoxGetConfiguration\", undefined, null);\n\n if (result.isSuccess && result.data) {\n return result.data;\n }\n\n throw new Error(result.errorMessage ?? \"Error fetching phone number configuration\");\n}\n\n/**\n * Fetch the configuration for phone numbers, SMS option, and possible phone number formats for different countries\n */\nasync function fetchPhoneNumberAndSmsConfiguration(): Promise {\n const options: PhoneNumberBoxGetConfigurationOptionsBag = {\n showSmsOptIn: true\n };\n const result = await http.post