https://domlink.deployments.hotsocket.fyi/
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);