Files
CT/src/Model/Utility/utility.ts
T

412 lines
11 KiB
TypeScript
Raw Normal View History

import { checkIsValidObject, checkMatchesObjectStructure } from "./typeSafety";
2026-09-06 01:50:56 +02:00
import * as Hammer from "hammerjs";
export interface Stringifiable {
2026-04-01 08:29:39 +00:00
toString(): string;
}
2024-07-27 17:41:54 +02:00
// crypto
export function generateRandomToken(length: number): string {
2026-04-01 08:29:39 +00:00
const array = new Uint8Array(length);
crypto.getRandomValues(array);
const string = array.join("");
return string.substring(0, length);
2024-07-27 17:41:54 +02:00
}
// date
export function createTimestamp(): string {
2026-04-01 08:29:39 +00:00
return new Date().toISOString();
}
2024-07-25 14:15:00 +02:00
// filters etc
export type StringEntryObject = { [key: string]: Stringifiable | undefined };
export function filterObjectsByStringEntries<T>(
2026-04-01 08:29:39 +00:00
reference: StringEntryObject,
converter: (object: T) => StringEntryObject,
objects: T[],
): Set<T> {
2026-04-01 08:29:39 +00:00
const matches: Set<T> = new Set();
object_loop: for (const object of objects) {
const doesMatch: boolean = checkDoesObjectMatchReference(
reference,
converter(object),
);
if (doesMatch) matches.add(object);
}
2026-04-01 08:29:39 +00:00
return matches;
}
2024-07-25 01:50:49 +02:00
export function checkDoesObjectMatchReference(
2026-04-01 08:29:39 +00:00
reference: StringEntryObject,
stringEntryObject: StringEntryObject,
explicitEmptyValue: boolean = false,
): boolean {
2026-04-01 08:29:39 +00:00
reference_entry_loop: for (const referenceEntry of Object.entries(
reference,
)) {
const [referenceKey, referenceValue] = referenceEntry;
const stringEntryObjectValue: Stringifiable | undefined =
stringEntryObject[referenceKey];
if (referenceValue == undefined) return false;
if (referenceValue[0] == "-") {
const strippedReferenceValue: string = referenceValue
.toString()
.substring(1);
// property may not exist
if (
strippedReferenceValue == "" &&
stringEntryObjectValue != undefined &&
stringEntryObjectValue != ""
) {
return false;
}
// property may not match
if (stringEntryObjectValue == strippedReferenceValue) {
return false;
}
} else {
if (explicitEmptyValue == false) {
// property must exist but be anything
if (
referenceValue == "" &&
(stringEntryObjectValue == undefined ||
stringEntryObjectValue == "")
) {
return false;
} else if (referenceValue == "") {
continue reference_entry_loop;
}
2026-04-01 08:29:39 +00:00
}
// property must match
if (stringEntryObjectValue != referenceValue) {
return false;
}
}
}
2026-04-01 08:29:39 +00:00
return true;
}
export function collectObjectValuesForKey<T>(
2026-04-01 08:29:39 +00:00
key: string,
converter: (object: T) => StringEntryObject,
objects: T[],
): string[] {
2026-04-01 08:29:39 +00:00
const values: Set<string> = new Set();
2026-04-01 08:29:39 +00:00
for (const object of objects) {
const stringEntryObject: StringEntryObject = converter(object);
const stringEntryObjectValue: Stringifiable | undefined =
stringEntryObject[key];
if (stringEntryObjectValue == undefined) continue;
2026-04-01 08:29:39 +00:00
values.add(stringEntryObjectValue.toString());
}
2026-04-01 08:29:39 +00:00
return [...values.values()];
}
2024-07-25 14:15:00 +02:00
export function filterObjectsByWords<T>(
2026-04-01 08:29:39 +00:00
query: string,
getStringsOfObject: (object: T) => string[],
objects: T[],
2024-07-25 14:15:00 +02:00
): Set<T> {
2026-04-01 08:29:39 +00:00
const matches: Set<T> = new Set();
object_loop: for (const object of objects) {
const doesMatch: boolean = checkDoesObjectMatchSearch(
query,
getStringsOfObject,
object,
);
if (doesMatch) matches.add(object);
}
2024-07-25 14:15:00 +02:00
2026-04-01 08:29:39 +00:00
return matches;
2024-07-25 14:15:00 +02:00
}
2024-07-25 15:31:27 +02:00
export function checkDoesObjectMatchSearch<T>(
2026-04-01 08:29:39 +00:00
query: string,
getStringsOfObject: (object: T) => string[],
object: T,
2024-07-25 14:15:00 +02:00
): boolean {
2026-04-01 08:29:39 +00:00
if (query == "") return true;
const stringsInObject: string[] = getStringsOfObject(object);
const wordsInObject: string[] = [];
for (const string of stringsInObject) {
const lowercaseWordsInString = string
.toString()
.toLowerCase()
.split(" ")
.filter((word) => word != "");
wordsInObject.push(...lowercaseWordsInString);
}
const lowercaseWordsInQuery = query
.toLowerCase()
.split(" ")
.filter((word) => word != "");
for (const queryWord of lowercaseWordsInQuery) {
if (queryWord[0] == "-") {
// exclusion
const wordContent = queryWord.substring(1);
if (wordsInObject.includes(wordContent)) {
return false;
}
} else {
if (wordsInObject.includes(queryWord) == false) {
return false;
}
}
2024-07-25 14:15:00 +02:00
}
2026-04-01 08:29:39 +00:00
return true;
2024-07-25 14:15:00 +02:00
}
// handlers
export type Handler<T> = (item: T) => void;
export class HandlerManager<T> {
handlers = new Map<string, Handler<T>>();
2026-04-01 08:29:39 +00:00
// manage
setHandler = (id: string, handler: Handler<T>): void => {
this.handlers.set(id, handler);
2026-04-01 08:29:39 +00:00
};
deleteHandler = (id: string): void => {
this.handlers.delete(id);
2026-04-01 08:29:39 +00:00
};
// trigger
trigger = (item: T): void => {
[...this.handlers.values()].forEach((handler) => handler(item));
2026-04-01 08:29:39 +00:00
};
2024-07-25 14:15:00 +02:00
}
2024-07-23 14:15:01 +02:00
// sorting
export class IndexManager<T> {
2026-04-01 08:29:39 +00:00
private itemToString: (item: T) => string;
2024-07-23 14:15:01 +02:00
2026-04-01 08:29:39 +00:00
sortedStrings: string[] = [];
2024-07-23 14:15:01 +02:00
2026-04-01 08:29:39 +00:00
// methods
update = (items: T[]): void => {
this.sortedStrings = [];
2024-07-23 14:15:01 +02:00
2026-04-01 08:29:39 +00:00
let strings: string[] = [];
for (const item of items) {
const string: string = this.itemToString(item);
strings.push(string);
}
2024-07-23 14:15:01 +02:00
2026-04-01 08:29:39 +00:00
this.sortedStrings = strings.sort(localeCompare);
};
2024-07-23 14:15:01 +02:00
2026-04-01 08:29:39 +00:00
getIndex = (item: T): number => {
const string: string = this.itemToString(item);
const index: number = this.sortedStrings.indexOf(string);
return index;
};
2024-07-23 14:15:01 +02:00
2026-04-01 08:29:39 +00:00
// init
constructor(itemToString: (item: T) => string) {
this.itemToString = itemToString;
}
2024-07-23 14:15:01 +02:00
}
// storage
export function getLocalStorageItemAndClear(key: string): string | null {
2026-04-01 08:29:39 +00:00
const value: string | null = localStorage.getItem(key);
localStorage.removeItem(key);
if (value != null) localStorage.setItem(`_${key}`, value);
return value;
}
// string & parsing
2024-07-19 17:56:18 +02:00
export function stringify(data: any): string {
2026-04-01 08:29:39 +00:00
return JSON.stringify(data, null, 4);
2024-07-19 17:56:18 +02:00
}
2024-07-24 14:34:28 +02:00
export function padZero(string: string | undefined, length: number): string {
2026-04-01 08:29:39 +00:00
return (string ?? "").padStart(length, "0");
2024-07-24 14:34:28 +02:00
}
2024-07-19 17:56:18 +02:00
export function parse(string: string): any {
2026-04-01 08:29:39 +00:00
try {
return JSON.parse(string);
} catch {
return {};
}
}
export function parseValidObject<T>(string: string, reference: T): T | null {
2026-04-01 08:29:39 +00:00
const parsed: any = parse(string);
if (checkIsValidObject(parsed) == false) return null;
2026-04-01 08:29:39 +00:00
const doesMatchReference: boolean = checkMatchesObjectStructure(
parsed,
reference,
);
if (doesMatchReference == false) return null;
2026-04-01 08:29:39 +00:00
return parsed;
}
2024-07-20 18:58:25 +02:00
export function parseOrFallback(inputString: string): any {
2026-04-01 08:29:39 +00:00
try {
return JSON.parse(inputString);
} catch {
return inputString;
}
}
export function parseArray(inputString: string): any[] {
2026-04-01 08:29:39 +00:00
const parsed: any = parseOrFallback(inputString);
if (Array.isArray(parsed) == false) return [];
return parsed;
}
2024-07-20 18:58:25 +02:00
// sort
export function localeCompare(a: string, b: string): number {
2026-04-01 08:29:39 +00:00
return a.localeCompare(b);
}
2026-08-30 16:15:14 +02:00
2026-08-30 22:24:55 +02:00
// ui
export function implementPinchZoom(canvas: HTMLElement) {
2026-08-30 16:15:14 +02:00
let pinching = false;
let lastElement: HTMLElement | undefined = undefined;
let initialDistance: number,
currentZoom: number,
initialZoom: number,
currentX: number,
currentY: number,
initialX: number,
initialY: number,
initialTouchX: number,
initialTouchY: number;
2026-08-30 22:43:56 +02:00
function reset() {
initialDistance = 0;
2026-08-30 22:43:56 +02:00
currentZoom = 1;
initialZoom = 1;
2026-08-30 22:43:56 +02:00
currentX = 0;
currentY = 0;
initialX = 0;
initialY = 0;
initialTouchX = 0;
initialTouchY = 0;
2026-08-30 22:43:56 +02:00
}
2026-08-30 16:15:14 +02:00
2026-08-30 22:43:56 +02:00
reset();
2026-08-30 22:24:55 +02:00
const MIN = 0.25;
const MAX = 5;
2026-09-09 00:04:27 +02:00
const element = (): HTMLElement|null=> canvas.querySelector(".zoom");
2026-08-30 22:24:55 +02:00
const distance = (e: TouchEvent) =>
2026-09-09 00:04:27 +02:00
Math.hypot(
e.touches[0].pageX - e.touches[1].pageX,
e.touches[0].pageY - e.touches[1].pageY,
);
2026-08-30 22:24:55 +02:00
const point = (e: TouchEvent, direction: "x" | "y", i: number) =>
2026-09-09 00:04:27 +02:00
e.touches[i][direction == "x" ? "clientX" : "clientY"];
2026-08-30 22:24:55 +02:00
const midpoint = (e: TouchEvent, direction: "x" | "y") =>
2026-09-09 00:04:27 +02:00
(point(e, direction, 0) + point(e, direction, 1)) / 2;
2026-08-30 22:24:55 +02:00
2026-09-06 01:50:56 +02:00
function apply(factor: number, _offset?: [number, number]) {
2026-09-09 00:04:27 +02:00
const el = element();
if (!el) return;
if (factor < MIN) return apply(MIN, _offset);
if (factor > MAX) return apply(5, _offset);
2026-09-06 01:50:56 +02:00
let x: number, y: number;
if (_offset) [x, y] = _offset;
else {
x = currentX
y = currentY
}
2026-09-09 00:04:27 +02:00
el.style.transform = `scale(${factor.toString()}) translate(${x}px, ${y}px)`;
2026-09-06 01:50:56 +02:00
currentX = x;
currentY = y;
currentZoom = factor;
2026-08-30 16:15:14 +02:00
}
2026-08-30 22:24:55 +02:00
canvas.addEventListener("wheel", (event: WheelEvent) => {
2026-09-06 01:50:56 +02:00
event.preventDefault();
initialZoom = currentZoom;
initialX = currentX;
initialY = currentY;
if (!event.shiftKey) {
apply(currentZoom, [
currentX - event.deltaX,
currentY - event.deltaY,
]);
return;
}
apply(currentZoom - event.deltaY * 0.01);
2026-08-30 22:24:55 +02:00
});
2026-09-09 00:04:27 +02:00
canvas.addEventListener("scroll", (event: Event) => {
2026-09-06 01:50:56 +02:00
event.preventDefault();
});
canvas.addEventListener("touchstart", (event: TouchEvent) => {
(document.activeElement as HTMLElement).blur();
2026-09-09 00:04:27 +02:00
const el = element();
if (!el) return;
if (lastElement != undefined && lastElement != el) reset();
lastElement = el;
2026-09-06 01:50:56 +02:00
initialZoom = currentZoom;
initialX = currentX;
initialY = currentY;
if (event.touches.length != 2) {
pinching = false;
initialTouchX = event.touches[0].clientX;
initialTouchY = event.touches[0].clientY;
return;
}
pinching = true;
initialDistance = distance(event);
initialTouchX = midpoint(event, "x");
initialTouchY = midpoint(event, "y");
});
canvas.addEventListener("touchmove", (event: TouchEvent) => {
if (!pinching) {
apply(currentZoom, [
initialX +
(event.touches[0].clientX - initialTouchX) / initialZoom,
initialY +
(event.touches[0].clientY - initialTouchY) / initialZoom,
]);
return;
}
if (event.touches.length < 2) return;
event.preventDefault();
const currentDistance = distance(event);
const midX = midpoint(event, "x");
const midY = midpoint(event, "y");
const ratio = currentDistance / initialDistance;
const difference = currentDistance - initialDistance;
apply(initialZoom * ratio, [
initialX + (midX - difference - initialTouchX) / initialZoom,
initialY + (midY - difference - initialTouchY) / initialZoom,
]);
2026-08-30 22:24:55 +02:00
});
}