mapcomplete/UI/BaseUIElement.ts

213 lines
6.3 KiB
TypeScript
Raw Normal View History

2021-06-10 01:36:20 +02:00
/**
* A thin wrapper around a html element, which allows to generate a HTML-element.
2021-06-23 02:14:15 +02:00
*
2021-06-10 01:36:20 +02:00
* Assumes a read-only configuration, so it has no 'ListenTo'
*/
2022-12-16 13:44:25 +01:00
import { Utils } from "../Utils"
2021-06-10 01:36:20 +02:00
export default abstract class BaseUIElement {
2022-09-08 21:40:48 +02:00
protected _constructedHtmlElement: HTMLElement
protected isDestroyed = false
protected readonly clss: Set<string> = new Set<string>()
protected style: string
private _onClick: () => void | Promise<void>
public onClick(f: () => void) {
this._onClick = f
2021-06-10 01:36:20 +02:00
this.SetClass("clickable")
2021-06-23 02:14:15 +02:00
if (this._constructedHtmlElement !== undefined) {
2022-09-08 21:40:48 +02:00
this._constructedHtmlElement.onclick = f
2021-06-10 01:36:20 +02:00
}
2022-09-08 21:40:48 +02:00
return this
2021-06-10 01:36:20 +02:00
}
2021-06-10 01:36:20 +02:00
AttachTo(divId: string) {
2022-09-08 21:40:48 +02:00
let element = document.getElementById(divId)
2021-06-10 01:36:20 +02:00
if (element === null) {
if (Utils.runningFromConsole) {
this.ConstructElement()
return
}
2022-09-08 21:40:48 +02:00
throw "SEVERE: could not attach UIElement to " + divId
2021-06-10 01:36:20 +02:00
}
let alreadyThere = false
const elementToAdd = this.ConstructElement()
const childs = Array.from(element.childNodes)
for (const child of childs) {
if (child === elementToAdd) {
alreadyThere = true
continue
}
element.removeChild(child)
2021-06-10 01:36:20 +02:00
}
if (elementToAdd !== undefined && !alreadyThere) {
element.appendChild(elementToAdd)
2021-06-10 01:36:20 +02:00
}
2022-09-08 21:40:48 +02:00
return this
2021-06-10 01:36:20 +02:00
}
public ScrollToTop() {
this._constructedHtmlElement?.scrollTo(0, 0)
}
2021-06-23 02:14:15 +02:00
2022-12-16 13:44:25 +01:00
public ScrollIntoView(options?: { onlyIfPartiallyHidden?: boolean }) {
if (this._constructedHtmlElement === undefined) {
return
}
2022-12-16 13:44:25 +01:00
let alignToTop = true
if (options?.onlyIfPartiallyHidden) {
// Is the element completely in the view?
2022-12-16 13:44:25 +01:00
const parentRect = Utils.findParentWithScrolling(
this._constructedHtmlElement.parentElement
).getBoundingClientRect()
const elementRect = this._constructedHtmlElement.getBoundingClientRect()
// Check if the element is within the vertical bounds of the parent element
const topIsVisible = elementRect.top >= parentRect.top
const bottomIsVisible = elementRect.bottom <= parentRect.bottom
2022-12-16 13:44:25 +01:00
const inView = topIsVisible && bottomIsVisible
if (inView) {
return
}
2022-12-16 13:44:25 +01:00
if (topIsVisible) {
alignToTop = false
}
}
this._constructedHtmlElement?.scrollIntoView({
behavior: "smooth",
2022-12-16 13:44:25 +01:00
block: "start",
})
}
2021-06-10 01:36:20 +02:00
/**
* Adds all the relevant classes, space separated
2021-06-10 01:36:20 +02:00
*/
public SetClass(clss: string) {
2021-11-07 16:34:51 +01:00
if (clss == undefined) {
2022-04-13 02:44:06 +02:00
return this
2021-11-07 16:34:51 +01:00
}
2022-09-08 21:40:48 +02:00
const all = clss.split(" ").map((clsName) => clsName.trim())
let recordedChange = false
2021-06-12 02:58:32 +02:00
for (let c of all) {
2022-09-08 21:40:48 +02:00
c = c.trim()
2021-06-10 01:36:20 +02:00
if (this.clss.has(clss)) {
2022-09-08 21:40:48 +02:00
continue
2021-06-10 01:36:20 +02:00
}
2021-06-23 02:14:15 +02:00
if (c === undefined || c === "") {
2022-09-08 21:40:48 +02:00
continue
2021-06-12 02:58:32 +02:00
}
2022-09-08 21:40:48 +02:00
this.clss.add(c)
recordedChange = true
2021-06-10 01:36:20 +02:00
}
if (recordedChange) {
2022-09-08 21:40:48 +02:00
this._constructedHtmlElement?.classList.add(...Array.from(this.clss))
2021-06-10 01:36:20 +02:00
}
2022-09-08 21:40:48 +02:00
return this
2021-06-10 01:36:20 +02:00
}
public RemoveClass(classes: string): BaseUIElement {
2022-09-08 21:40:48 +02:00
const all = classes.split(" ").map((clsName) => clsName.trim())
for (let clss of all) {
if (this.clss.has(clss)) {
2022-09-08 21:40:48 +02:00
this.clss.delete(clss)
this._constructedHtmlElement?.classList.remove(clss)
}
2021-06-10 01:36:20 +02:00
}
2022-09-08 21:40:48 +02:00
return this
2021-06-10 01:36:20 +02:00
}
2021-06-23 02:14:15 +02:00
public HasClass(clss: string): boolean {
return this.clss.has(clss)
}
2021-06-10 01:36:20 +02:00
public SetStyle(style: string): BaseUIElement {
2022-09-08 21:40:48 +02:00
this.style = style
2021-06-23 02:14:15 +02:00
if (this._constructedHtmlElement !== undefined) {
2022-09-08 21:40:48 +02:00
this._constructedHtmlElement.style.cssText = style
2021-06-10 01:36:20 +02:00
}
2022-09-08 21:40:48 +02:00
return this
2021-06-10 01:36:20 +02:00
}
2021-06-23 02:14:15 +02:00
2021-06-10 01:36:20 +02:00
/**
* The same as 'Render', but creates a HTML element instead of the HTML representation
*/
public ConstructElement(): HTMLElement {
if (typeof window === undefined) {
2022-09-08 21:40:48 +02:00
return undefined
2021-06-10 01:36:20 +02:00
}
if (this._constructedHtmlElement !== undefined) {
return this._constructedHtmlElement
}
try {
2022-09-08 21:40:48 +02:00
const el = this.InnerConstructElement()
2021-06-10 01:36:20 +02:00
2021-06-23 02:14:15 +02:00
if (el === undefined) {
2022-09-08 21:40:48 +02:00
return undefined
2021-06-10 01:36:20 +02:00
}
2022-09-08 21:40:48 +02:00
this._constructedHtmlElement = el
2021-06-23 02:14:15 +02:00
const style = this.style
if (style !== undefined && style !== "") {
el.style.cssText = style
}
2022-07-20 12:04:14 +02:00
if (this.clss?.size > 0) {
2021-06-23 02:14:15 +02:00
try {
el.classList.add(...Array.from(this.clss))
} catch (e) {
2022-09-08 21:40:48 +02:00
console.error(
"Invalid class name detected in:",
Array.from(this.clss).join(" "),
"\nErr msg is ",
e
)
2021-06-10 01:36:20 +02:00
}
}
2021-06-23 02:14:15 +02:00
if (this._onClick !== undefined) {
2022-09-08 21:40:48 +02:00
const self = this
2022-08-22 19:16:37 +02:00
el.onclick = async (e) => {
2021-06-23 02:14:15 +02:00
// @ts-ignore
if (e.consumed) {
2022-09-08 21:40:48 +02:00
return
2021-06-23 02:14:15 +02:00
}
2022-09-08 21:40:48 +02:00
const v = self._onClick()
if (typeof v === "object") {
2022-08-22 19:16:37 +02:00
await v
}
2021-06-23 02:14:15 +02:00
// @ts-ignore
2022-09-08 21:40:48 +02:00
e.consumed = true
2021-06-23 02:14:15 +02:00
}
2022-09-08 21:40:48 +02:00
el.classList.add("pointer-events-none", "cursor-pointer")
2021-06-23 02:14:15 +02:00
}
2021-06-10 01:36:20 +02:00
2021-06-23 02:14:15 +02:00
return el
} catch (e) {
2022-09-08 21:40:48 +02:00
const domExc = e as DOMException
2021-06-23 02:14:15 +02:00
if (domExc) {
2022-10-27 01:50:01 +02:00
console.error(
"An exception occured",
domExc.code,
domExc.message,
domExc.name,
domExc
)
2021-06-16 14:23:53 +02:00
}
console.error(e)
2021-06-23 02:14:15 +02:00
}
2021-06-10 01:36:20 +02:00
}
2021-06-23 02:14:15 +02:00
public AsMarkdown(): string {
throw "AsMarkdown is not implemented; implement it in the subclass"
}
2022-01-26 21:40:38 +01:00
public Destroy() {
2022-09-08 21:40:48 +02:00
this.isDestroyed = true
2022-01-06 18:51:52 +01:00
}
2021-06-23 02:14:15 +02:00
2022-09-08 21:40:48 +02:00
protected abstract InnerConstructElement(): HTMLElement
}