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 = {}; protected classList: Set = new Set(); protected eventHandlers: ((eventType: GefestElementEvents, 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(event, data))); } on(event: GefestElementEvents, handler: (data : unknown) => void) { this.eventHandlers.push((calledEvent: GefestElementEvents, calledData: unknown) => { if (calledEvent === event) return handler(calledData); }); } once(event: GefestElementEvents, handler: (data : unknown) => void) { const addedHandler = (calledEvent: GefestElementEvents, calledData: unknown) => { if (calledEvent === event) { const index = this.eventHandlers.indexOf(addedHandler); this.eventHandlers.splice(index, 1); return handler(calledData); } }; this.eventHandlers.push(addedHandler); } 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 `
${content}
`; } /** * 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(item); } else if (item instanceof GefestElement) { if (!item.style && this.style) { item.style = this.style; } rendered.push(item.build()); } } return this.wrapHTML(rendered.join('')); } }