summaryrefslogtreecommitdiffstats
path: root/apps/web-shared/src/lib/helpers.ts
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web-shared/src/lib/helpers.ts')
-rw-r--r--apps/web-shared/src/lib/helpers.ts489
1 files changed, 489 insertions, 0 deletions
diff --git a/apps/web-shared/src/lib/helpers.ts b/apps/web-shared/src/lib/helpers.ts
new file mode 100644
index 0000000..650bccf
--- /dev/null
+++ b/apps/web-shared/src/lib/helpers.ts
@@ -0,0 +1,489 @@
+import {StorageKeys} from "$shared/lib/configuration";
+import {TimeEntryDto} from "$shared/lib/models/TimeEntryDto";
+import {UnwrappedEntryDateTime} from "$shared/lib/models/UnwrappedEntryDateTime";
+import {Temporal} from "@js-temporal/polyfill";
+
+export const EMAIL_REGEX = new RegExp(/^([a-z0-9]+(?:([._\-])[a-z0-9]+)*@(?:[a-z0-9]+(?:(-)[a-z0-9]+)?\.)+[a-z0-9](?:[a-z0-9]*[a-z0-9])?)$/i);
+export const URL_REGEX = new RegExp(/^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-.][a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$/gm);
+export const GUID_REGEX = new RegExp(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i);
+export const NORWEGIAN_PHONE_NUMBER_REGEX = new RegExp(/(0047|\+47|47)?\d{8,12}/);
+
+export function is_email(value: string): boolean {
+ return EMAIL_REGEX.test(String(value).toLowerCase());
+}
+
+export function is_url(value: string): boolean {
+ return URL_REGEX.test(String(value).toLowerCase());
+}
+
+export function is_norwegian_phone_number(value: string): boolean {
+ if (value.length < 8 || value.length > 12) {
+ return false;
+ }
+ return NORWEGIAN_PHONE_NUMBER_REGEX.test(String(value));
+}
+
+export function switch_theme() {
+ const html = document.querySelector("html");
+ if (html) {
+ if (html.dataset.theme === "dark") {
+ html.dataset.theme = "light";
+ } else {
+ html.dataset.theme = "dark";
+ }
+ localStorage.setItem(StorageKeys.theme, html.dataset.theme);
+ }
+}
+
+export function unwrap_date_time_from_entry(entry: TimeEntryDto): UnwrappedEntryDateTime {
+ if (!entry) throw new Error("entry was undefined");
+ const currentTimeZone = Temporal.Now.timeZone().id;
+ const startInstant = Temporal.Instant.from(entry.start).toZonedDateTimeISO(currentTimeZone);
+ const stopInstant = Temporal.Instant.from(entry.stop).toZonedDateTimeISO(currentTimeZone);
+
+ return {
+ start_date: startInstant.toPlainDate(),
+ stop_date: stopInstant.toPlainDate(),
+ start_time: startInstant.toPlainTime(),
+ stop_time: stopInstant.toPlainTime(),
+ duration: Temporal.Duration.from({
+ hours: stopInstant.hour,
+ minutes: stopInstant.minute
+ }).subtract(Temporal.Duration.from({
+ hours: startInstant.hour,
+ minutes: startInstant.minute
+ }))
+ };
+}
+
+
+export function is_guid(value: string): boolean {
+ if (!value) {
+ return false;
+ }
+ if (value[0] === "{") {
+ value = value.substring(1, value.length - 1);
+ }
+ return GUID_REGEX.test(value);
+}
+
+export function is_empty_object(obj: object): boolean {
+ return obj !== void 0 && Object.keys(obj).length > 0;
+}
+
+export function merge_obj_arr<T>(a: Array<T>, b: Array<T>, props: Array<string>): Array<T> {
+ let start = 0;
+ let merge = [];
+
+ while (start < a.length) {
+
+ if (a[start] === b[start]) {
+ //pushing the merged objects into array
+ merge.push({...a[start], ...b[start]});
+ }
+ //incrementing start value
+ start = start + 1;
+ }
+ return merge;
+}
+
+export function set_favicon(url: string) {
+ // Find the current favicon element
+ const favicon = document.querySelector("link[rel=\"icon\"]") as HTMLLinkElement;
+ if (favicon) {
+ // Update the new link
+ favicon.href = url;
+ } else {
+ // Create new `link`
+ const link = document.createElement("link");
+ link.rel = "icon";
+ link.href = url;
+
+ // Append to the `head` element
+ document.head.appendChild(link);
+ }
+}
+
+export function set_emoji_favicon(emoji: string) {
+ // Create a canvas element
+ const canvas = document.createElement("canvas");
+ canvas.height = 64;
+ canvas.width = 64;
+
+ // Get the canvas context
+ const context = canvas.getContext("2d") as CanvasRenderingContext2D;
+ context.font = "64px serif";
+ context.fillText(emoji, 0, 64);
+
+ // Get the custom URL
+ const url = canvas.toDataURL();
+
+ // Update the favicon
+ set_favicon(url);
+}
+
+
+// https://stackoverflow.com/a/48400665/11961742
+export function seconds_to_hour_minute_string(seconds: number) {
+ const hours = Math.floor(seconds / (60 * 60));
+ seconds -= hours * (60 * 60);
+ const minutes = Math.floor(seconds / 60);
+ return hours + "h" + minutes + "m";
+}
+
+export function get_query_string(params: any = {}): string {
+ const map = Object.keys(params).reduce((arr: Array<string>, key: string) => {
+ if (params[key] !== undefined) {
+ return arr.concat(`${key}=${encodeURIComponent(params[key])}`);
+ }
+ return arr;
+ }, [] as any);
+
+ if (map.length) {
+ return `?${map.join("&")}`;
+ }
+
+ return "";
+}
+
+export function make_url(url: string, params: object): string {
+ return `${url}${get_query_string(params)}`;
+}
+
+export function load_script(url: string) {
+ unload_script(url, () => {
+ return new Promise(function (resolve, reject) {
+ const script = document.createElement("script");
+ script.src = url;
+
+ script.addEventListener("load", function () {
+ // The script is loaded completely
+ resolve(true);
+ });
+
+ document.body.appendChild(script);
+ });
+ });
+}
+
+export function unload_script(src: string, callback?: Function): void {
+ document.querySelectorAll("script[src='" + src + "']").forEach(el => el.remove());
+ if (typeof callback === "function") {
+ callback();
+ }
+}
+
+export function noop() {
+}
+
+export async function run_async(functionToRun: Function): Promise<any> {
+ return new Promise((greatSuccess, graveFailure) => {
+ try {
+ greatSuccess(functionToRun());
+ } catch (exception) {
+ graveFailure(exception);
+ }
+ });
+}
+
+// https://stackoverflow.com/a/45215694/11961742
+export function get_selected_options(domElement: HTMLSelectElement): Array<string> {
+ const ret = [];
+
+ // fast but not universally supported
+ if (domElement.selectedOptions !== undefined) {
+ for (let i = 0; i < domElement.selectedOptions.length; i++) {
+ ret.push(domElement.selectedOptions[i].value);
+ }
+
+ // compatible, but can be painfully slow
+ } else {
+ for (let i = 0; i < domElement.options.length; i++) {
+ if (domElement.options[i].selected) {
+ ret.push(domElement.options[i].value);
+ }
+ }
+ }
+ return ret;
+}
+
+export function uuid_v4(): string {
+ // @ts-ignore
+ return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16));
+}
+
+export function random_string(length: number): string {
+ if (!length) {
+ throw new Error("length is undefined");
+ }
+ let result = "";
+ const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+ const charactersLength = characters.length;
+ for (let i = 0; i < length; i++) {
+ result += characters.charAt(Math.floor(Math.random() * charactersLength));
+ }
+ return result;
+}
+
+interface CreateElementOptions {
+ name: string,
+ properties?: object,
+ children?: Array<HTMLElement|Function|Node>
+}
+
+export function create_element_from_object(elementOptions: CreateElementOptions): HTMLElement {
+ return create_element(elementOptions.name, elementOptions.properties, elementOptions.children);
+}
+
+export function create_element(name: string, properties?: object, children?: Array<HTMLElement|any>): HTMLElement {
+ if (!name || name.length < 1) {
+ throw new Error("name is required");
+ }
+ const node = document.createElement(name);
+ if (properties) {
+ for (const [key, value] of Object.entries(properties)) {
+ // @ts-ignore
+ node[key] = value;
+ }
+ }
+
+ if (children && children.length > 0) {
+ let actualChildren = children;
+ if (typeof children === "function") {
+ // @ts-ignore
+ actualChildren = children();
+ }
+ for (const child of actualChildren) {
+ node.appendChild(child as Node);
+ }
+ }
+ return node;
+}
+
+export function get_element_position(element: HTMLElement|any) {
+ if (!element) return {x: 0, y: 0};
+ let x = 0;
+ let y = 0;
+ while (true) {
+ x += element.offsetLeft;
+ y += element.offsetTop;
+ if (element.offsetParent === null) {
+ break;
+ }
+ element = element.offsetParent;
+ }
+ return {x, y};
+}
+
+export function restrict_input_to_numbers(element: HTMLElement, specials: Array<string> = [], mergeSpecialsWithDefaults: boolean = false): void {
+ if (element) {
+ element.addEventListener("keydown", (e) => {
+ const defaultSpecials = ["Backspace", "ArrowLeft", "ArrowRight", "Tab",];
+ let keys = specials.length > 0 ? specials : defaultSpecials;
+ if (mergeSpecialsWithDefaults && specials) {
+ keys = [...specials, ...defaultSpecials];
+ }
+ if (keys.indexOf(e.key) !== -1) {
+ return;
+ }
+ if (isNaN(parseInt(e.key))) {
+ e.preventDefault();
+ }
+ });
+ }
+}
+
+export function element_has_focus(element: HTMLElement): boolean {
+ return element === document.activeElement;
+}
+
+export function move_focus(element: HTMLElement): void {
+ if (!element) {
+ element = document.getElementsByTagName("body")[0];
+ }
+ element.focus();
+ // @ts-ignore
+ if (!element_has_focus(element)) {
+ element.setAttribute("tabindex", "-1");
+ element.focus();
+ }
+}
+
+export function get_url_parameter(name: string): string {
+ // @ts-ignore
+ return new RegExp("[?&]" + name + "=([^&#]*)")?.exec(window.location.href)[1];
+}
+
+export function update_url_parameter(param: string, newVal: string): void {
+ let newAdditionalURL = "";
+ let tempArray = location.href.split("?");
+ const baseURL = tempArray[0];
+ const additionalURL = tempArray[1];
+ let temp = "";
+ if (additionalURL) {
+ tempArray = additionalURL.split("&");
+ for (let i = 0; i < tempArray.length; i++) {
+ if (tempArray[i].split("=")[0] !== param) {
+ newAdditionalURL += temp + tempArray[i];
+ temp = "&";
+ }
+ }
+ }
+ const rows_txt = temp + "" + param + "=" + newVal;
+ const newUrl = baseURL + "?" + newAdditionalURL + rows_txt;
+ window.history.replaceState("", "", newUrl);
+}
+
+
+export function get_style_string(rules: CSSRuleList) {
+ let styleString = "";
+ for (const [key, value] of Object.entries(rules)) {
+ styleString += key + ":" + value + ";";
+ }
+ return styleString;
+}
+
+export function get_local_time_zone_date(date: Date): Date {
+ const timeOffsetInMS = new Date().getTimezoneOffset() * 60000;
+ date.setTime(date.getTime() - timeOffsetInMS);
+ return date;
+}
+
+export function parse_iso_local(s: string) {
+ const b = s.split(/\D/);
+ //@ts-ignore
+ return new Date(b[0], b[1] - 1, b[2], b[3], b[4], b[5]);
+}
+
+export function resolve_references(json: any) {
+ if (!json) return;
+ if (typeof json === "string") {
+ json = JSON.parse(json ?? "{}");
+ }
+ const byid = {}, refs = [];
+ json = function recurse(obj, prop, parent) {
+ if (typeof obj !== "object" || !obj) {
+ return obj;
+ }
+ if (Object.prototype.toString.call(obj) === "[object Array]") {
+ for (let i = 0; i < obj.length; i++) {
+ if (typeof obj[i] !== "object" || !obj[i]) {
+ continue;
+ } else if ("$ref" in obj[i]) {
+ // @ts-ignore
+ obj[i] = recurse(obj[i], i, obj);
+ } else {
+ obj[i] = recurse(obj[i], prop, obj);
+ }
+ }
+ return obj;
+ }
+ if ("$ref" in obj) {
+ let ref = obj.$ref;
+ if (ref in byid) {
+ // @ts-ignore
+ return byid[ref];
+ }
+ refs.push([parent, prop, ref]);
+ return;
+ } else if ("$id" in obj) {
+ let id = obj.$id;
+ delete obj.$id;
+ if ("$values" in obj) {
+ obj = obj.$values.map(recurse);
+ } else {
+ for (let prop2 in obj) {
+ // @ts-ignore
+ obj[prop2] = recurse(obj[prop2], prop2, obj);
+ }
+ }
+ // @ts-ignore
+ byid[id] = obj;
+ }
+ return obj;
+ }(json);
+ for (let i = 0; i < refs.length; i++) {
+ let ref = refs[i];
+ // @ts-ignore
+ ref[0][ref[1]] = byid[ref[2]];
+ }
+ return json;
+}
+
+export function to_readable_date_string(date: Date, locale = "nb-NO"): string {
+ date.setMinutes(date.getMinutes() - date.getTimezoneOffset());
+ return date.toLocaleString(locale);
+}
+
+export function get_random_int(min: number, max: number): number {
+ min = Math.ceil(min);
+ max = Math.floor(max);
+ return Math.floor(Math.random() * (max - min + 1)) + min;
+}
+
+export function to_readable_bytes(bytes: number): string {
+ const s = ["bytes", "kB", "MB", "GB", "TB", "PB"];
+ const e = Math.floor(Math.log(bytes) / Math.log(1024));
+ return (bytes / Math.pow(1024, e)).toFixed(2) + " " + s[e];
+}
+
+export function can_use_dom(): boolean {
+ return !!(typeof window !== "undefined" && window.document && window.document.createElement);
+}
+
+export function session_storage_remove_regex(regex: RegExp): void {
+ let n = sessionStorage.length;
+ while (n--) {
+ const key = sessionStorage.key(n);
+ if (key && regex.test(key)) {
+ sessionStorage.removeItem(key);
+ }
+ }
+}
+
+export function local_storage_remove_regex(regex: RegExp): void {
+ let n = localStorage.length;
+ while (n--) {
+ const key = localStorage.key(n);
+ if (key && regex.test(key)) {
+ localStorage.removeItem(key);
+ }
+ }
+}
+
+export function session_storage_set_json(key: string, value: object): void {
+ sessionStorage.setItem(key, JSON.stringify(value));
+}
+
+export function session_storage_get_json(key: string): object {
+ return JSON.parse(sessionStorage.getItem(key) ?? "{}");
+}
+
+export function local_storage_set_json(key: string, value: object): void {
+ localStorage.setItem(key, JSON.stringify(value));
+}
+
+export function local_storage_get_json(key: string): object {
+ return JSON.parse(localStorage.getItem(key) ?? "{}");
+}
+
+export function get_hash_code(value: string): number|undefined {
+ let hash = 0;
+ if (value.length === 0) {
+ return;
+ }
+ for (let i = 0; i < value.length; i++) {
+ const char = value.charCodeAt(i);
+ hash = (hash << 5) - hash + char;
+ hash |= 0;
+ }
+ return hash;
+}
+
+export function $(selector: string): HTMLElement|null {
+ return document.querySelector(selector);
+}
+
+export function $$(selector: string): NodeListOf<Element> {
+ return document.querySelectorAll(selector);
+}