https://domlink.deployments.hotsocket.fyi/
at main 247 lines 6.8 kB view raw
1let _LCounter = 0; 2const NodeMap = new WeakMap<HTMLElement, Node>(); 3 4/** Core wrapper for {@link HTMLElement DOM elements}. */ 5export class Node { 6 wraps: HTMLElement; 7 children: Node[] = []; 8 id: string = "L" + _LCounter++; 9 /** 10 * Creates a node, wrapping an HTML element. 11 * @example new Node(document.createElement("h1")); 12 */ 13 constructor(wraps: HTMLElement) { 14 this.wraps = wraps; 15 this.wraps.id = this.id; 16 } 17 /** Adds a child node. Should not be overridden. */ 18 add(node: NodeEquivalent) { 19 if (node != undefined) { 20 let xnode: Node; 21 if (typeof node === "string") { 22 xnode = new Label(node); 23 } else { 24 xnode = node; 25 } 26 this.children.push(xnode); 27 this.wraps.appendChild(xnode.wraps); 28 } else { 29 throw new Error("node undefined"); 30 } 31 return this; 32 } 33 /** Works similarly to {@link add}, but takes multiple nodes and is the one you would want to override. */ 34 with(...nodes: NodeEquivalent[]) { 35 nodes.forEach(x=>{this.add(x)}); 36 return this; 37 } 38 /** Removes a child node in the same way {@link add} does. */ 39 delete(node: Node) { 40 let idx = this.children.indexOf(node); 41 if (idx < 0) { 42 throw new Error(`Node ${node.id} not found.`); 43 } 44 this.children.splice(idx, 1); 45 this.wraps.removeChild(node.wraps); 46 } 47 /** Removes one or more child nodes, like {@link with} does adding them. */ 48 remove(...nodes: Node[]) { 49 nodes.forEach(x=>{this.delete(x)}); 50 return this; 51 } 52 /** Conveniently chain-able callback thing for styling your nodes. 53 * @example new Label("This is a label!").style((s)=>{s.color="maroon";}) */ 54 style(fn: (style: CSSStyleDeclaration) => void) { 55 fn(this.wraps.style); 56 return this; 57 } 58 /** Adds a CSS class to your node. 59 * @example new Container().class("my-class") */ 60 class(cls: string) { 61 this.wraps.classList.add(cls); 62 return this; 63 } 64 /** Hopefully, this points at the parent element. */ 65 get parent(): Node | undefined{ 66 if (this.wraps.parentElement == null) return undefined; 67 return NodeMap.get(this.wraps.parentElement); 68 } 69} 70type NodeEquivalent = (Node | string); 71enum LinkTarget { 72 THIS_TAB = "_this", 73 NEW_TAB = "_blank", 74 OUT_FRAME = "_parent", 75 NOT_FRAME = "_top" 76} 77/** Wrapper for {@link HTMLAnchorElement `<a>`} */ 78export class Link extends Node { 79 constructor(around: NodeEquivalent | null) { 80 let a = document.createElement("a"); 81 a.classList.add("LLink"); 82 super(a); 83 if (around != null) { 84 this.add(around); 85 } 86 } 87 set destination(x: string) { 88 (this.wraps as HTMLAnchorElement).href = x; 89 } 90 // noinspection JSUnusedGlobalSymbols 91 get destination(): string { 92 return (this.wraps as HTMLAnchorElement).href; 93 } 94 set target(x: string) { 95 (this.wraps as HTMLAnchorElement).target = x; 96 } 97 get target(): string { 98 return (this.wraps as HTMLAnchorElement).target; 99 } 100 to(dst: string, tgt: LinkTarget = LinkTarget.NEW_TAB) { 101 this.destination = dst; 102 this.target = tgt; 103 return this; 104 } 105 106} 107/** Wrapper for {@link HTMLDivElement `<div>`} */ 108export class Container extends Node { 109 constructor() { 110 let div = document.createElement("div"); 111 div.classList.add("LContainer"); 112 super(div); 113 } 114} 115/** Vertical Flexbox container */ 116export class Column extends Container { 117 constructor() { 118 super(); 119 this.wraps.classList.add("LColumn"); 120 } 121} 122/** Horizontal Flexbox container */ 123export class Row extends Container { 124 constructor() { 125 super(); 126 this.wraps.classList.add("LRow"); 127 } 128} 129/** Wrapper for {@link HTMLTableElement `<table>`} */ 130export class Table extends Node { 131 constructor() { 132 let table = document.createElement("table"); 133 table.classList.add("LTable"); 134 super(table); 135 } 136} 137/** Wrapper for {@link HTMLTableRowElement `<tr>`} */ 138export class TableRow extends Node { 139 constructor() { 140 let tr = document.createElement("tr"); 141 tr.classList.add("LTableRow"); 142 super(tr); 143 } 144} 145/** Wrapper for {@link HTMLTableCellElement `<td>`} */ 146export class TableCell extends Node { 147 constructor() { 148 let td = document.createElement("td"); 149 td.classList.add("LTableCell"); 150 super(td); 151 } 152} 153/** An "abstract" class of sorts for {@link HTMLElement}s that have a textContent member. */ 154export class Text extends Node { 155 private _text: string = ""; 156 get text() { 157 return this._text; 158 } 159 set text(x) { 160 this._text = x; 161 this.wraps.textContent = x; 162 } 163} 164/** Wrapper for {@link HTMLSpanElement `<span>`} */ 165export class Label extends Text { 166 constructor(text: string = "") { 167 let el = document.createElement("span"); 168 el.classList.add("LLabel"); 169 super(el); 170 this.text = text; 171 } 172} 173/** Wrapper for {@link HTMLButtonElement `<button>`} */ 174export class Button extends Text { 175 constructor(label: string, action: EventListener, asLink: boolean = false) { 176 let btn = document.createElement(asLink ? "a" : "button"); 177 if (asLink) btn.classList.add("LBtnLink"); 178 btn.textContent = label; 179 btn.addEventListener("click", action); 180 btn.classList.add("LButton"); 181 super(btn); 182 this.text = label; 183 } 184} 185/** Interface providing {@link Updatable.watch} */ 186interface Updatable<T> { 187 /** Takes a callback `watcher` that is called when something like a text input is updated. */ 188 watch: (watcher: (newValue: T)=>void) => void; 189} 190export class Input extends Text implements Updatable<string> { 191 constructor(type: HTMLInputElement["type"] = "text") { 192 let el = document.createElement("input"); 193 el.type = type; 194 el.classList.add("LInput"); 195 super(el); 196 } 197 set placeholder(x: string) { 198 (this.wraps as HTMLInputElement).placeholder = x; 199 } 200 get value(): string { 201 return (this.wraps as HTMLInputElement).value; 202 } 203 set value(x: string) { 204 (this.wraps as HTMLInputElement).value = x; 205 this.watching.forEach(y=>y(x)); 206 } 207 208 209 210 watching: ((newValue: string) => void)[] = []; 211 /** Implementation for {@link Updatable.watch} */ 212 watch(watcher: (newValue: string)=>void) { 213 this.watching.push(watcher); 214 this.wraps.oninput = ()=>{watcher(this.value)}; 215 watcher(this.value); 216 } 217} 218/** Wrapper for {@link HTMLImageElement `<img>`} */ 219export class Image extends Node { 220 constructor(src: string) { 221 let img = document.createElement("img"); 222 img.src = src; 223 img.classList.add("LImage"); 224 super(img); 225 } 226} 227/** Wrapper for {@link HTMLDialogElement `<dialog>`} with methods to show/hide it as a modal. */ 228export class Modal extends Node { 229 constructor() { 230 let dlg: HTMLDialogElement = document.createElement("dialog"); 231 dlg.classList.add("LModal"); 232 super(dlg); 233 } 234 /** Shows the dialog as a modal. */ 235 show() { 236 Body.add(this); 237 (this.wraps as HTMLDialogElement).showModal(); 238 } 239 /** Hides the dialog. */ 240 hide() { 241 (this.wraps as HTMLDialogElement).close(); 242 Body.wraps.removeChild(this.wraps); 243 } 244} 245 246/** Convenience wrapper for the {@link HTMLBodyElement body} of the document. */ 247export const Body = new Node(document.body);