website/frontend/src/modules/Gefest/element.ts
2026-04-27 03:05:39 +03:00

232 lines
7.4 KiB
TypeScript

import { GefestEngine } from './engine';
import { GefestStyle } from './style';
// The general class of Gefest
type gefestElementEvents = "onClick";
export abstract class GefestElement {
style: GefestStyle | null = null;
isHidden: boolean = false;
//onClick: (() => void) | null = null;
readonly gefestId: string;
protected content: (GefestElement | string)[] | string;
protected attributes: Record<string, string> = {};
protected classList: Set<string> = new Set();
//protected eventHandlers: Record<string, (eventType: string, eventData: unknown) => void> = {};
protected eventHandlers: ((eventType: string, eventData: unknown) => void)[] = [];
protected clickHandler: (() => void) | null = null;
emit(event: gefestElementEvents, data: unknown = undefined) {
for (let handler of this.eventHandlers)
new Promise((rs) => rs(handler("onClick", data)));
}
set onClick(handler: (() => void) | null) {
if (handler) {
this.clickHandler = handler;
} else {
this.clickHandler = null;
}
}
get onClick(): (() => void) | null {
if (this.clickHandler === null)
return null;
return () => {
this.clickHandler!();
this.emit("onClick");
GefestEngine.activateOnClick();
};
}
/**
* Creates a new instance of the element from an HTML string.
* @param html The HTML string to parse.
* @returns The created GefestElement.
*/
static fromHTML(html: string): GefestElement {
const htmlElement = new DOMParser().parseFromString(html, 'text/html').body.firstChild as HTMLElement;
const element = new (class extends GefestElement {
protected wrapHTML(content: string): string {
return content;
}
})(htmlElement.innerHTML);
for (const attr of htmlElement.attributes) {
try {
element.setAttribute(attr.name, attr.value);
} catch (_) {}
}
for (const className of htmlElement.classList) {
element.addClass(className);
}
return element;
}
/**
* Creates a new instance of the element.
* @param content The content for the element. Can be a string, an array of strings, an array of GefestElements or a combination of both.
*/
constructor (content : (GefestElement | string)[] | string = []) {
this.gefestId = GefestEngine.register(this);
this.attributes["data-gefest-id"] = this.gefestId;
this.content = content;
}
/**
* Call to GefestEngine for rerender element
*/
update (): void {
GefestEngine.reRenderByGI(this.gefestId);
}
/**
* Builds the element and returns it as a string. If the element has a style, it applies the style to the rendered HTML.
* @param isFromStyle Indicates if the build is being called from a style application. This prevents infinite recursion when applying styles.
* @returns The built element as a string.
*/
build (isFromStyle: boolean = false): string {
if (this.isHidden)
this.setAttribute('hidden', '');
else
this.removeAttribute('hidden');
if (!isFromStyle)
return this.applyStyle(this.render(), this);
return this.render();
};
/**
* Checks if an attribute key is reserved.
* @param key The attribute key.
*/
checkReservedAttribute(key: string): void {
if (({ "style": true, "class": true, "data-gefest-id": true })[key])
throw new Error(`The attribute "${
key
}" is reserved. Use the setPersonalStyle method for styles and setClass method for classes.`);
}
/**
* Sets a personal style for the element. This will be applied on top of any style that is set for the element.
* @param style The personal style to set. If null, the personal style will be removed.
*/
setPersonalStyle(style: string | null): void {
if (style === null) {
delete this.attributes.style;
return;
}
this.attributes.style = style;
}
/**
* Adds a class to the element's class list and updates the "class" attribute accordingly.
* @param className The class name to add.
*/
addClass(className: string): void {
this.classList.add(className);
this.attributes.class = Array.from(this.classList).join(' ');
}
/**
* Removes a class from the element's class list and updates the "class" attribute accordingly.
* @param className The class name to remove.
*/
removeClass(className: string): void {
this.classList.delete(className);
this.attributes.class = Array.from(this.classList).join(' ');
}
/**
* Gets the list of classes for the element.
* @returns The array of class names.
*/
getClassList(): string[] {
return Array.from(this.classList);
}
/**
* Sets an attribute for the element.
* @param key The attribute key.
* @param value The attribute value.
*/
setAttribute (key: string, value: string): void {
this.checkReservedAttribute(key);
this.attributes[key] = value;
}
/**
* Gets an attribute value by its key.
* @param key The attribute key.
* @returns The attribute value or undefined if not found.
*/
getAttribute (key: string): string | undefined {
return this.attributes[key];
}
/**
* Removes an attribute by its key.
* @param key The attribute key.
*/
removeAttribute (key: string): void {
this.checkReservedAttribute(key);
delete this.attributes[key];
}
/**
* Applies the element's style to the given HTML.
* @param html The HTML to apply the style to.
* @returns The styled HTML.
*/
protected applyStyle(html: string, element?: GefestElement): string {
if (this.style)
return this.style.applyStyle(html, element);
return html;
}
/**
* Converts the element's attributes to a string.
* @returns The attributes string.
*/
protected attributesToString (): string {
return Object.entries(this.attributes)
.map(([key, value]) => `${key}="${value}"`)
.join(' ');
}
/**
* Wraps the given content in HTML tags.
* @param content The content to wrap.
* @returns The wrapped HTML.
*/
protected wrapHTML (content: string): string {
const attrString = this.attributesToString();
return `<div ${attrString}>${content}</div>`;
}
/**
* Renders the element and returns it as a string.
* @returns The rendered element as a string.
*/
protected render (): string {
if (typeof this.content === 'string') {
return this.wrapHTML(this.content);
}
const rendered : string[] = [];
for (const item of this.content) {
if (typeof item === 'string') {
rendered.push(this.applyStyle(this.wrapHTML(item)));
}
else if (item instanceof GefestElement) {
if (!item.style && this.style) {
item.style = this.style;
}
rendered.push(item.build());
}
}
return this.wrapHTML(rendered.join(''));
}
}