2024-07-22 01:40:00 +02:00
|
|
|
import { checkIsValidObject, checkMatchesObjectStructure } from "./typeSafety";
|
2024-07-21 22:42:32 +02:00
|
|
|
|
2024-07-27 00:33:42 +02:00
|
|
|
export interface Stringifiable {
|
2026-04-01 08:29:39 +00:00
|
|
|
toString(): string;
|
2024-07-27 00:33:42 +02:00
|
|
|
}
|
|
|
|
|
|
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);
|
2026-04-01 13:40:07 +00:00
|
|
|
const string = array.join("");
|
|
|
|
|
return string.substring(0, length);
|
2024-07-27 17:41:54 +02:00
|
|
|
}
|
|
|
|
|
|
2024-07-19 19:49:55 +02:00
|
|
|
// date
|
|
|
|
|
export function createTimestamp(): string {
|
2026-04-01 08:29:39 +00:00
|
|
|
return new Date().toISOString();
|
2024-07-19 19:49:55 +02:00
|
|
|
}
|
|
|
|
|
|
2024-07-25 14:15:00 +02:00
|
|
|
// filters etc
|
2024-07-27 00:33:42 +02:00
|
|
|
export type StringEntryObject = { [key: string]: Stringifiable | undefined };
|
2024-07-25 01:42:22 +02:00
|
|
|
|
2024-07-24 23:34:11 +02:00
|
|
|
export function filterObjectsByStringEntries<T>(
|
2026-04-01 08:29:39 +00:00
|
|
|
reference: StringEntryObject,
|
|
|
|
|
converter: (object: T) => StringEntryObject,
|
|
|
|
|
objects: T[],
|
2024-07-25 00:27:08 +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 = checkDoesObjectMatchReference(
|
|
|
|
|
reference,
|
|
|
|
|
converter(object),
|
|
|
|
|
);
|
|
|
|
|
if (doesMatch) matches.add(object);
|
|
|
|
|
}
|
2024-07-24 17:37:18 +02:00
|
|
|
|
2026-04-01 08:29:39 +00:00
|
|
|
return matches;
|
2024-07-24 17:37:18 +02:00
|
|
|
}
|
|
|
|
|
|
2024-07-25 01:50:49 +02:00
|
|
|
export function checkDoesObjectMatchReference(
|
2026-04-01 08:29:39 +00:00
|
|
|
reference: StringEntryObject,
|
|
|
|
|
stringEntryObject: StringEntryObject,
|
2026-04-01 09:33:30 +00:00
|
|
|
explicitEmptyValue: boolean = false,
|
2024-07-25 01:20:48 +02:00
|
|
|
): 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 {
|
2026-04-01 09:33:30 +00:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-07-25 01:20:48 +02:00
|
|
|
}
|
2026-04-01 08:29:39 +00:00
|
|
|
return true;
|
2024-07-25 01:20:48 +02:00
|
|
|
}
|
|
|
|
|
|
2024-07-25 00:26:40 +02:00
|
|
|
export function collectObjectValuesForKey<T>(
|
2026-04-01 08:29:39 +00:00
|
|
|
key: string,
|
|
|
|
|
converter: (object: T) => StringEntryObject,
|
|
|
|
|
objects: T[],
|
2024-07-25 00:26:40 +02:00
|
|
|
): string[] {
|
2026-04-01 08:29:39 +00:00
|
|
|
const values: Set<string> = new Set();
|
2024-07-25 00:26:40 +02:00
|
|
|
|
2026-04-01 08:29:39 +00:00
|
|
|
for (const object of objects) {
|
|
|
|
|
const stringEntryObject: StringEntryObject = converter(object);
|
|
|
|
|
const stringEntryObjectValue: Stringifiable | undefined =
|
|
|
|
|
stringEntryObject[key];
|
2026-04-01 09:33:30 +00:00
|
|
|
if (stringEntryObjectValue == undefined) continue;
|
2024-07-25 00:26:40 +02:00
|
|
|
|
2026-04-01 08:29:39 +00:00
|
|
|
values.add(stringEntryObjectValue.toString());
|
|
|
|
|
}
|
2024-07-25 00:26:40 +02:00
|
|
|
|
2026-04-01 08:29:39 +00:00
|
|
|
return [...values.values()];
|
2024-07-25 00:26:40 +02:00
|
|
|
}
|
|
|
|
|
|
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> {
|
2026-04-13 20:27:18 +00:00
|
|
|
handlers = new Map<string, Handler<T>>();
|
2026-04-01 08:29:39 +00:00
|
|
|
|
|
|
|
|
// manage
|
2026-04-13 20:27:18 +00:00
|
|
|
setHandler = (id: string, handler: Handler<T>): void => {
|
|
|
|
|
this.handlers.set(id, handler);
|
2026-04-01 08:29:39 +00:00
|
|
|
};
|
|
|
|
|
|
2026-04-13 20:27:18 +00:00
|
|
|
deleteHandler = (id: string): void => {
|
|
|
|
|
this.handlers.delete(id);
|
2026-04-01 08:29:39 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// trigger
|
|
|
|
|
trigger = (item: T): void => {
|
2026-04-13 20:27:18 +00:00
|
|
|
[...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
|
|
|
}
|
|
|
|
|
|
2024-07-27 00:33:42 +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;
|
2024-07-27 00:33:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 {};
|
|
|
|
|
}
|
2024-07-21 22:42:32 +02:00
|
|
|
}
|
|
|
|
|
|
2024-07-22 01:40:00 +02:00
|
|
|
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;
|
2024-07-22 01:40:00 +02:00
|
|
|
|
2026-04-01 08:29:39 +00:00
|
|
|
const doesMatchReference: boolean = checkMatchesObjectStructure(
|
|
|
|
|
parsed,
|
|
|
|
|
reference,
|
|
|
|
|
);
|
|
|
|
|
if (doesMatchReference == false) return null;
|
2024-07-22 01:40:00 +02:00
|
|
|
|
2026-04-01 08:29:39 +00:00
|
|
|
return parsed;
|
2024-07-19 19:49:55 +02:00
|
|
|
}
|
2024-07-20 18:58:25 +02:00
|
|
|
|
2024-07-27 00:33:42 +02:00
|
|
|
export function parseOrFallback(inputString: string): any {
|
2026-04-01 08:29:39 +00:00
|
|
|
try {
|
|
|
|
|
return JSON.parse(inputString);
|
|
|
|
|
} catch {
|
|
|
|
|
return inputString;
|
|
|
|
|
}
|
2024-07-27 00:33:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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-27 00:33:42 +02:00
|
|
|
}
|
|
|
|
|
|
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);
|
2024-07-21 22:42:32 +02:00
|
|
|
}
|
2026-08-30 16:15:14 +02:00
|
|
|
|
2026-08-30 22:24:55 +02:00
|
|
|
// ui
|
|
|
|
|
export function implementPinchZoom(targetSelector: string) {
|
2026-08-30 16:15:14 +02:00
|
|
|
let pinching = false;
|
2026-08-30 22:43:56 +02:00
|
|
|
let lastElement: HTMLElement|undefined = undefined;
|
|
|
|
|
|
|
|
|
|
let initialDistance,
|
|
|
|
|
currentZoom, initialZoom,
|
|
|
|
|
currentX, currentY,
|
|
|
|
|
initialX, initialY,
|
|
|
|
|
initialTouchX, initialTouchY;
|
|
|
|
|
|
|
|
|
|
function reset() {
|
|
|
|
|
initialDistance = 0;
|
|
|
|
|
|
|
|
|
|
currentZoom = 1;
|
|
|
|
|
initialZoom = 1;
|
|
|
|
|
|
|
|
|
|
currentX = 0;
|
|
|
|
|
currentY = 0;
|
|
|
|
|
initialX = 0;
|
|
|
|
|
initialY = 0;
|
|
|
|
|
initialTouchX = 0;
|
|
|
|
|
initialTouchY = 0;
|
|
|
|
|
}
|
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;
|
|
|
|
|
|
|
|
|
|
const element = (): HTMLElement => canvas.querySelector(targetSelector);
|
|
|
|
|
const parent = (): HTMLElement => element().parentElement;
|
|
|
|
|
const distance = (e: TouchEvent) =>
|
2026-08-30 22:43:56 +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-08-30 22:43:56 +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-08-30 22:43:56 +02:00
|
|
|
(point(e, direction, 0) + point(e, direction, 1)) / 2;
|
2026-08-30 22:24:55 +02:00
|
|
|
|
|
|
|
|
function apply(factor: number, offset: [number, number]) {
|
2026-08-30 22:43:56 +02:00
|
|
|
if (factor < MIN) return apply(MIN, offset);
|
|
|
|
|
if (factor > MAX) return apply(5, offset);
|
|
|
|
|
|
|
|
|
|
const [x, y] = offset;
|
|
|
|
|
element().style.transform = `scale(${(Math.floor(factor * 100) / 100).toString()}) translate(${x}px, ${y}px)`;
|
|
|
|
|
currentX = x;
|
|
|
|
|
currentY = y;
|
|
|
|
|
currentZoom = factor;
|
2026-08-30 16:15:14 +02:00
|
|
|
}
|
|
|
|
|
|
2026-08-30 22:24:55 +02:00
|
|
|
const canvas = document.body;
|
|
|
|
|
canvas.addEventListener("touchstart", (event: TouchEvent) => {
|
|
|
|
|
(document.activeElement as HTMLElement).blur();
|
|
|
|
|
|
2026-08-30 22:43:56 +02:00
|
|
|
if (lastElement != undefined && lastElement != element()) reset();
|
|
|
|
|
lastElement = element();
|
|
|
|
|
|
|
|
|
|
initialZoom = currentZoom;
|
2026-08-30 22:24:55 +02:00
|
|
|
initialX = currentX;
|
|
|
|
|
initialY = currentY;
|
|
|
|
|
|
2026-08-30 22:43:56 +02:00
|
|
|
if (event.touches.length != 2) {
|
2026-08-30 22:24:55 +02:00
|
|
|
pinching = false;
|
|
|
|
|
initialTouchX = event.touches[0].clientX;
|
|
|
|
|
initialTouchY = event.touches[0].clientY;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-30 16:15:14 +02:00
|
|
|
pinching = true;
|
2026-08-30 22:24:55 +02:00
|
|
|
initialDistance = distance(event);
|
|
|
|
|
initialTouchX = midpoint(event, "x");
|
|
|
|
|
initialTouchY = midpoint(event, "y");
|
2026-08-30 16:15:14 +02:00
|
|
|
});
|
2026-08-30 22:24:55 +02:00
|
|
|
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;
|
2026-08-30 16:15:14 +02:00
|
|
|
event.preventDefault();
|
|
|
|
|
const currentDistance = distance(event);
|
2026-08-30 22:24:55 +02:00
|
|
|
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]);
|
|
|
|
|
});
|
|
|
|
|
canvas.addEventListener("wheel", (event: WheelEvent) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
if (!event.shiftKey) {
|
|
|
|
|
apply(currentZoom, [currentX - event.deltaX, currentY - event.deltaY]);
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
apply(currentZoom + (event.deltaY < 0 ? 1 : -1) * 0.1, [initialX, initialY]);
|
|
|
|
|
});
|
|
|
|
|
canvas.addEventListener("scroll", (event: MouseEvent) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
});
|
|
|
|
|
}
|